11
11
12
12
import { parse } from '@babel/parser' ;
13
13
import { enableHookNameParsing } from 'react-devtools-feature-flags' ;
14
+ import LRU from 'lru-cache' ;
14
15
import { SourceMapConsumer } from 'source-map' ;
15
16
import { getHookName , isNonDeclarativePrimitiveHook } from './astUtils' ;
16
17
import { sourceMapsAreAppliedToErrors } from './ErrorTester' ;
@@ -21,20 +22,22 @@ import type {
21
22
HookSource ,
22
23
HooksTree ,
23
24
} 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' ;
25
26
import type { Thenable } from 'shared/ReactTypes' ;
26
27
import type { SourceConsumer , SourceMap } from './astUtils' ;
27
28
28
29
const SOURCE_MAP_REGEX = / ? s o u r c e M a p p i n g U R L = ( [ ^ \s ' " ] + ) / gm;
29
30
const ABSOLUTE_URL_REGEX = / ^ h t t p s ? : \/ \/ / i;
30
31
const MAX_SOURCE_LENGTH = 100_000_000 ;
31
32
33
+ type AST = mixed ;
34
+
32
35
type HookSourceData = { |
33
36
// Generated by react-debug-tools.
34
37
hookSource : HookSource ,
35
38
36
39
// AST for original source code; typically comes from a consumed source map.
37
- originalSourceAST : mixed ,
40
+ originalSourceAST : AST | null ,
38
41
39
42
// Source code (React components or custom hooks) containing primitive hook calls.
40
43
// If no source map has been provided, this code will be the same as runtimeSourceCode.
@@ -54,6 +57,32 @@ type HookSourceData = {|
54
57
sourceMapContents : SourceMap | null ,
55
58
| } ;
56
59
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
+
57
86
export default async function parseHookNames (
58
87
hooksTree : HooksTree ,
59
88
) : Thenable < HookNames | null > {
@@ -85,28 +114,44 @@ export default async function parseHookNames(
85
114
throw Error ( 'Hook source code location not found.' ) ;
86
115
} else {
87
116
if ( ! fileNameToHookSourceData . has ( fileName ) ) {
88
- fileNameToHookSourceData . set ( fileName , {
117
+ const hookSourceData : HookSourceData = {
89
118
hookSource,
90
119
originalSourceAST : null ,
91
120
originalSourceCode : null ,
92
121
runtimeSourceCode : null ,
93
122
sourceConsumer : null ,
94
123
sourceMapURL : null ,
95
124
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 ) ;
97
147
}
98
148
}
99
149
}
100
150
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
-
107
151
return loadSourceFiles ( fileNameToHookSourceData )
108
152
. then ( ( ) => extractAndLoadSourceMaps ( fileNameToHookSourceData ) )
109
153
. then ( ( ) => parseSourceAST ( fileNameToHookSourceData ) )
154
+ . then ( ( ) => updateLruCache ( fileNameToHookSourceData ) )
110
155
. then ( ( ) => findHookNames ( hooksList , fileNameToHookSourceData ) ) ;
111
156
}
112
157
@@ -129,6 +174,11 @@ function extractAndLoadSourceMaps(
129
174
) : Promise < * > {
130
175
const promises = [ ] ;
131
176
fileNameToHookSourceData . forEach ( hookSourceData => {
177
+ if ( hookSourceData . originalSourceAST !== null ) {
178
+ // Use cached metadata.
179
+ return ;
180
+ }
181
+
132
182
const runtimeSourceCode = ( ( hookSourceData . runtimeSourceCode : any ) : string ) ;
133
183
const sourceMappingURLs = runtimeSourceCode . match ( SOURCE_MAP_REGEX ) ;
134
184
if ( sourceMappingURLs == null ) {
@@ -360,6 +410,11 @@ async function parseSourceAST(
360
410
361
411
const promises = [ ] ;
362
412
fileNameToHookSourceData . forEach ( hookSourceData => {
413
+ if ( hookSourceData . originalSourceAST !== null ) {
414
+ // Use cached metadata.
415
+ return ;
416
+ }
417
+
363
418
const { runtimeSourceCode, sourceMapContents} = hookSourceData ;
364
419
if ( sourceMapContents !== null ) {
365
420
// Parse and extract the AST from the source map.
@@ -386,9 +441,9 @@ async function parseSourceAST(
386
441
console . groupEnd ( ) ;
387
442
}
388
443
389
- // Save the original source and parsed AST for later.
390
- // TODO (named hooks) Cache this across components, per source/file name.
391
444
hookSourceData . originalSourceCode = originalSourceCode ;
445
+
446
+ // TODO Parsing should ideally be done off of the main thread.
392
447
hookSourceData . originalSourceAST = parse ( originalSourceCode , {
393
448
sourceType : 'unambiguous' ,
394
449
plugins : [ 'jsx' , 'typescript' ] ,
@@ -399,6 +454,8 @@ async function parseSourceAST(
399
454
} else {
400
455
// There's no source map to parse here so we can just parse the original source itself.
401
456
hookSourceData . originalSourceCode = runtimeSourceCode ;
457
+
458
+ // TODO Parsing should ideally be done off of the main thread.
402
459
hookSourceData . originalSourceAST = parse ( runtimeSourceCode , {
403
460
sourceType : 'unambiguous' ,
404
461
plugins : [ 'jsx' , 'typescript' ] ,
@@ -420,3 +477,25 @@ function flattenHooksList(
420
477
}
421
478
}
422
479
}
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
+ }
0 commit comments