Skip to content

Commit 9f7a22f

Browse files
author
Brian Vaughn
committed
Added an LRU cache and eviction strategy for source code/ASTs
1 parent 615d8e4 commit 9f7a22f

File tree

10 files changed

+113
-29
lines changed

10 files changed

+113
-29
lines changed

packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ describe('parseHookNames', () => {
266266
); // external source map
267267
});
268268

269-
// TODO (named hooks) Inline require (e.g. require("react").useState()) isn't supported yet
269+
// TODO Inline require (e.g. require("react").useState()) isn't supported yet.
270270
// Maybe this isn't an important use case to support,
271271
// since inline requires are most likely to exist in compiled source (if at all).
272272
xit('should work for inline requires', async () => {

packages/react-devtools-extensions/src/astUtils.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,6 @@ function getFilteredHookASTNodes(
6969
potentialHooksFound: Array<NodePath>,
7070
source: string,
7171
): Array<NodePath> {
72-
// Remove targetHook from potentialHooks array since its purpose is served.
73-
// Also to clean the potentialHooks array for further filtering member nodes down the line.
74-
const hookIdx = potentialHooksFound.indexOf(potentialReactHookASTNode);
75-
if (hookIdx !== -1) {
76-
// TODO (named hooks) Should we be doing this? Is it necessary?
77-
// potentialHooksFound.splice(hookIdx, 1);
78-
}
79-
8072
let nodesAssociatedWithReactHookASTNode: NodePath[] = [];
8173
if (nodeContainsHookVariableName(potentialReactHookASTNode)) {
8274
// made custom hooks to enter this, always
@@ -275,7 +267,9 @@ function isHook(node: Node): boolean {
275267
isPascalCaseNameSpace.test(obj.name)
276268
);
277269
} else {
278-
// TODO (named hooks) Handle inline require statements e.g. require("useStable")(...)
270+
// TODO Possibly handle inline require statements e.g. require("useStable")(...)
271+
// This does not seem like a high priority, since inline requires are probably
272+
// not common and are also typically in compiled code rather than source code.
279273

280274
return false;
281275
}

packages/react-devtools-extensions/src/parseHookNames.js

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import {parse} from '@babel/parser';
1313
import {enableHookNameParsing} from 'react-devtools-feature-flags';
14+
import LRU from 'lru-cache';
1415
import {SourceMapConsumer} from 'source-map';
1516
import {getHookName, isNonDeclarativePrimitiveHook} from './astUtils';
1617
import {sourceMapsAreAppliedToErrors} from './ErrorTester';
@@ -21,20 +22,22 @@ import type {
2122
HookSource,
2223
HooksTree,
2324
} from 'react-debug-tools/src/ReactDebugHooks';
24-
import type {HookNames} from 'react-devtools-shared/src/types';
25+
import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
2526
import type {Thenable} from 'shared/ReactTypes';
2627
import type {SourceConsumer, SourceMap} from './astUtils';
2728

2829
const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm;
2930
const ABSOLUTE_URL_REGEX = /^https?:\/\//i;
3031
const MAX_SOURCE_LENGTH = 100_000_000;
3132

33+
type AST = mixed;
34+
3235
type HookSourceData = {|
3336
// Generated by react-debug-tools.
3437
hookSource: HookSource,
3538

3639
// AST for original source code; typically comes from a consumed source map.
37-
originalSourceAST: mixed,
40+
originalSourceAST: AST | null,
3841

3942
// Source code (React components or custom hooks) containing primitive hook calls.
4043
// If no source map has been provided, this code will be the same as runtimeSourceCode.
@@ -54,6 +57,32 @@ type HookSourceData = {|
5457
sourceMapContents: SourceMap | null,
5558
|};
5659

60+
type CachedMetadata = {|
61+
originalSourceAST: AST,
62+
originalSourceCode: string,
63+
sourceConsumer: SourceConsumer | null,
64+
|};
65+
66+
// On large trees, encoding takes significant time.
67+
// Try to reuse the already encoded strings.
68+
const fileNameToMetadataCache: LRUCache<string, CachedMetadata> = new LRU({
69+
max: 50,
70+
dispose: (fileName: string, metadata: CachedMetadata) => {
71+
if (__DEBUG__) {
72+
console.log(
73+
'fileNameToHookSourceData.dispose() Evicting cached metadata for "' +
74+
fileName +
75+
'"',
76+
);
77+
}
78+
79+
const sourceConsumer = metadata.sourceConsumer;
80+
if (sourceConsumer !== null) {
81+
sourceConsumer.destroy();
82+
}
83+
},
84+
});
85+
5786
export default async function parseHookNames(
5887
hooksTree: HooksTree,
5988
): Thenable<HookNames | null> {
@@ -85,28 +114,44 @@ export default async function parseHookNames(
85114
throw Error('Hook source code location not found.');
86115
} else {
87116
if (!fileNameToHookSourceData.has(fileName)) {
88-
fileNameToHookSourceData.set(fileName, {
117+
const hookSourceData: HookSourceData = {
89118
hookSource,
90119
originalSourceAST: null,
91120
originalSourceCode: null,
92121
runtimeSourceCode: null,
93122
sourceConsumer: null,
94123
sourceMapURL: null,
95124
sourceMapContents: null,
96-
});
125+
};
126+
127+
// If we've already loaded source/source map info for this file,
128+
// we can skip reloading it (and more importantly, re-parsing it).
129+
const metadata = fileNameToMetadataCache.get(fileName);
130+
if (metadata != null) {
131+
if (__DEBUG__) {
132+
console.groupCollapsed(
133+
'parseHookNames() Found cached metadata for file "' +
134+
fileName +
135+
'"',
136+
);
137+
console.log(metadata);
138+
console.groupEnd();
139+
}
140+
141+
hookSourceData.originalSourceAST = metadata.originalSourceAST;
142+
hookSourceData.originalSourceCode = metadata.originalSourceCode;
143+
hookSourceData.sourceConsumer = metadata.sourceConsumer;
144+
}
145+
146+
fileNameToHookSourceData.set(fileName, hookSourceData);
97147
}
98148
}
99149
}
100150

101-
// TODO (named hooks) Call .destroy() on SourceConsumers after we're done to free up memory.
102-
103-
// TODO (named hooks) Replace Array of hook names with a Map() of hook ID to name to better support mapping nested hook to name.
104-
// This is a little tricky though, since custom hooks don't have IDs.
105-
// We may need to add an ID for these too, in the backend?
106-
107151
return loadSourceFiles(fileNameToHookSourceData)
108152
.then(() => extractAndLoadSourceMaps(fileNameToHookSourceData))
109153
.then(() => parseSourceAST(fileNameToHookSourceData))
154+
.then(() => updateLruCache(fileNameToHookSourceData))
110155
.then(() => findHookNames(hooksList, fileNameToHookSourceData));
111156
}
112157

@@ -129,6 +174,11 @@ function extractAndLoadSourceMaps(
129174
): Promise<*> {
130175
const promises = [];
131176
fileNameToHookSourceData.forEach(hookSourceData => {
177+
if (hookSourceData.originalSourceAST !== null) {
178+
// Use cached metadata.
179+
return;
180+
}
181+
132182
const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
133183
const sourceMappingURLs = runtimeSourceCode.match(SOURCE_MAP_REGEX);
134184
if (sourceMappingURLs == null) {
@@ -360,6 +410,11 @@ async function parseSourceAST(
360410

361411
const promises = [];
362412
fileNameToHookSourceData.forEach(hookSourceData => {
413+
if (hookSourceData.originalSourceAST !== null) {
414+
// Use cached metadata.
415+
return;
416+
}
417+
363418
const {runtimeSourceCode, sourceMapContents} = hookSourceData;
364419
if (sourceMapContents !== null) {
365420
// Parse and extract the AST from the source map.
@@ -386,9 +441,9 @@ async function parseSourceAST(
386441
console.groupEnd();
387442
}
388443

389-
// Save the original source and parsed AST for later.
390-
// TODO (named hooks) Cache this across components, per source/file name.
391444
hookSourceData.originalSourceCode = originalSourceCode;
445+
446+
// TODO Parsing should ideally be done off of the main thread.
392447
hookSourceData.originalSourceAST = parse(originalSourceCode, {
393448
sourceType: 'unambiguous',
394449
plugins: ['jsx', 'typescript'],
@@ -399,6 +454,8 @@ async function parseSourceAST(
399454
} else {
400455
// There's no source map to parse here so we can just parse the original source itself.
401456
hookSourceData.originalSourceCode = runtimeSourceCode;
457+
458+
// TODO Parsing should ideally be done off of the main thread.
402459
hookSourceData.originalSourceAST = parse(runtimeSourceCode, {
403460
sourceType: 'unambiguous',
404461
plugins: ['jsx', 'typescript'],
@@ -420,3 +477,25 @@ function flattenHooksList(
420477
}
421478
}
422479
}
480+
481+
function updateLruCache(
482+
fileNameToHookSourceData: Map<string, HookSourceData>,
483+
): Promise<*> {
484+
fileNameToHookSourceData.forEach(
485+
({originalSourceAST, originalSourceCode, sourceConsumer}, fileName) => {
486+
// Only set once to avoid triggering eviction/cleanup code.
487+
if (!fileNameToMetadataCache.has(fileName)) {
488+
if (__DEBUG__) {
489+
console.log('updateLruCache() Caching metada for "' + fileName + '"');
490+
}
491+
492+
fileNameToMetadataCache.set(fileName, {
493+
originalSourceAST,
494+
originalSourceCode: ((originalSourceCode: any): string),
495+
sourceConsumer,
496+
});
497+
}
498+
},
499+
);
500+
return Promise.resolve();
501+
}

packages/react-devtools-shared/src/devtools/views/Components/Badge.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function Badge({
2626
type,
2727
children,
2828
}: Props) {
29-
if (hocDisplayNames === null) {
29+
if (hocDisplayNames === null || hocDisplayNames.length === 0) {
3030
return null;
3131
}
3232

packages/react-devtools-shared/src/devtools/views/Components/Components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@
6868
justify-content: center;
6969
font-size: var(--font-size-sans-large);
7070
color: var(--color-dim);
71+
border-left: 1px solid var(--color-border);
7172
}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,5 @@
6464
padding: 0.25rem;
6565
color: var(--color-dimmer);
6666
font-style: italic;
67+
border-left: 1px solid var(--color-border);
6768
}
68-
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.Wrapper {
2-
border-left: 1px solid var(--color-border);
2+
height: 100%;
33
}

packages/react-devtools-shared/src/hookNamesCache.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ export function loadHookNames(
7171

7272
const map = getRecordMap();
7373

74-
// TODO (named hooks) Make sure caching layer works and we aren't re-parsing ASTs.
7574
let record = map.get(element);
7675
if (record) {
77-
// TODO (named hooks) Do we need to update the Map to use new hooks list as keys?
78-
// or wil these be stable between inspections as a component updates?
76+
// TODO Do we need to update the Map to use new the hooks list objects as keys
77+
// or will these be stable between inspections as a component updates?
78+
// It seems like they're stable.
7979
} else {
8080
const callbacks = new Set();
8181
const wakeable: Wakeable = {

packages/react-devtools-shared/src/types.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,9 @@ export type ComponentFilter =
8181

8282
export type HookName = string | null;
8383
export type HookNames = Map<HooksNode, HookName>;
84+
85+
export type LRUCache<K, V> = {|
86+
get: (key: K) => V,
87+
has: (key: K) => boolean,
88+
set: (key: K, value: V) => void,
89+
|};

packages/react-devtools-shared/src/utils.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,17 @@ import {
4747
} from 'react-devtools-shared/src/types';
4848
import {localStorageGetItem, localStorageSetItem} from './storage';
4949
import {meta} from './hydration';
50+
5051
import type {ComponentFilter, ElementType} from './types';
52+
import type {LRUCache} from 'react-devtools-shared/src/types';
5153

5254
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
5355

5456
// On large trees, encoding takes significant time.
5557
// Try to reuse the already encoded strings.
56-
const encodedStringCache = new LRU({max: 1000});
58+
const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
59+
max: 1000,
60+
});
5761

5862
export function alphaSortKeys(
5963
a: string | number | Symbol,

0 commit comments

Comments
 (0)