Added performance timings to DevTools named hooks parsing (#22173)
Brian Vaughn committed
Aug 25, 2021 at 15:39 UTC
8456457c8d7766984b1bc030b14efb890ef47be2
3 files changed
+203
-11
packages/react-devtools-extensions/src/astUtils.js
+39
-1
@@ -7,6 +7,7 @@
7
* @flow
8
*/
9
10
+import {__PERFORMANCE_PROFILE__} from 'react-devtools-shared/src/constants';
11
import traverse, {NodePath, Node} from '@babel/traverse';
12
import {File} from '@babel/types';
13
@@ -27,6 +28,15 @@ export type SourceFileASTWithHookDetails = {
28
29
export const NO_HOOK_NAME = '<no-hook>';
30
31
+function mark(markName: string): void {
32
+ performance.mark(markName + '-start');
33
+}
34
+
35
+function measure(markName: string): void {
36
+ performance.mark(markName + '-end');
37
+ performance.measure(markName, markName + '-start', markName + '-end');
38
+}
39
+
40
const AST_NODE_TYPES = Object.freeze({
41
PROGRAM: 'Program',
42
CALL_EXPRESSION: 'CallExpression',
@@ -131,7 +141,13 @@ export function getHookName(
141
originalSourceLineNumber: number,
142
originalSourceColumnNumber: number,
143
): string | null {
144
+ if (__PERFORMANCE_PROFILE__) {
145
+ mark('getPotentialHookDeclarationsFromAST(originalSourceAST)');
146
+ }
147
const hooksFromAST = getPotentialHookDeclarationsFromAST(originalSourceAST);
148
+ if (__PERFORMANCE_PROFILE__) {
149
+ measure('getPotentialHookDeclarationsFromAST(originalSourceAST)');
150
+ }
151
152
let potentialReactHookASTNode = null;
153
if (originalSourceColumnNumber === 0) {
@@ -144,6 +160,7 @@ export function getHookName(
160
node,
161
originalSourceLineNumber,
162
);
163
+
164
const hookDeclaractionCheck = isConfirmedHookDeclaration(node);
165
return nodeLocationCheck && hookDeclaractionCheck;
166
});
@@ -158,6 +175,7 @@ export function getHookName(
175
originalSourceLineNumber,
176
originalSourceColumnNumber,
177
);
178
+
179
const hookDeclaractionCheck = isConfirmedHookDeclaration(node);
180
return nodeLocationCheck && hookDeclaractionCheck;
181
});
@@ -170,17 +188,31 @@ export function getHookName(
188
// nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
189
// depending on the type of potentialReactHookASTNode
190
try {
191
+ if (__PERFORMANCE_PROFILE__) {
192
+ mark('getFilteredHookASTNodes()');
193
+ }
194
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
195
potentialReactHookASTNode,
196
hooksFromAST,
197
originalSourceCode,
198
);
199
+ if (__PERFORMANCE_PROFILE__) {
200
+ measure('getFilteredHookASTNodes()');
201
+ }
202
179
- return getHookNameFromNode(
203
+ if (__PERFORMANCE_PROFILE__) {
204
+ mark('getHookNameFromNode()');
205
+ }
206
+ const name = getHookNameFromNode(
207
hook,
208
nodesAssociatedWithReactHookASTNode,
209
potentialReactHookASTNode,
210
);
211
+ if (__PERFORMANCE_PROFILE__) {
212
+ measure('getHookNameFromNode()');
213
+ }
214
+
215
+ return name;
216
} catch (error) {
217
console.error(error);
218
return null;
@@ -283,6 +315,9 @@ function getHookVariableName(
315
316
function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
317
const potentialHooksFound: NodePath[] = [];
318
+ if (__PERFORMANCE_PROFILE__) {
319
+ mark('traverse(sourceAST)');
320
+ }
321
traverse(sourceAST, {
322
enter(path) {
323
if (path.isVariableDeclarator() && isPotentialHookDeclaration(path)) {
@@ -290,6 +325,9 @@ function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
325
}
326
},
327
});
328
+ if (__PERFORMANCE_PROFILE__) {
329
+ measure('traverse(sourceAST)');
330
+ }
331
return potentialHooksFound;
332
}
333
packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js
+161
-10
@@ -12,7 +12,10 @@ import LRU from 'lru-cache';
12
import {SourceMapConsumer} from 'source-map-js';
13
import {getHookName} from '../astUtils';
14
import {areSourceMapsAppliedToErrors} from '../ErrorTester';
15
-import {__DEBUG__} from 'react-devtools-shared/src/constants';
15
+import {
16
+ __DEBUG__,
17
+ __PERFORMANCE_PROFILE__,
18
+} from 'react-devtools-shared/src/constants';
19
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
20
import {sourceMapIncludesSource} from '../SourceMapUtils';
21
import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
@@ -26,6 +29,15 @@ import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
29
import type {Thenable} from 'shared/ReactTypes';
30
import type {SourceConsumer} from '../astUtils';
31
32
+function mark(markName: string): void {
33
+ performance.mark(markName + '-start');
34
+}
35
+
36
+function measure(markName: string): void {
37
+ performance.mark(markName + '-end');
38
+ performance.measure(markName, markName + '-start', markName + '-end');
39
+}
40
+
41
const MAX_SOURCE_LENGTH = 100_000_000;
42
43
type AST = mixed;
@@ -107,8 +119,15 @@ const originalURLToMetadataCache: LRUCache<
119
export async function parseHookNames(
120
hooksTree: HooksTree,
121
): Thenable<HookNames | null> {
122
+ if (__PERFORMANCE_PROFILE__) {
123
+ mark('parseHookNames()');
124
+ mark('flattenHooksList()');
125
+ }
126
const hooksList: Array<HooksNode> = [];
127
flattenHooksList(hooksTree, hooksList);
128
+ if (__PERFORMANCE_PROFILE__) {
129
+ measure('flattenHooksList()');
130
+ }
131
132
if (__DEBUG__) {
133
console.log('parseHookNames() hooksList:', hooksList);
@@ -164,11 +183,56 @@ export async function parseHookNames(
183
}
184
}
185
167
- return loadSourceFiles(locationKeyToHookSourceData)
168
- .then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData))
169
- .then(() => parseSourceAST(locationKeyToHookSourceData))
170
- .then(() => updateLruCache(locationKeyToHookSourceData))
171
- .then(() => findHookNames(hooksList, locationKeyToHookSourceData));
186
+ if (__PERFORMANCE_PROFILE__) {
187
+ mark('loadSourceFiles()');
188
+ }
189
+
190
+ let promise = loadSourceFiles(locationKeyToHookSourceData);
191
+ if (__PERFORMANCE_PROFILE__) {
192
+ promise = promise.then(data => {
193
+ mark('extractAndLoadSourceMaps()');
194
+ measure('loadSourceFiles()');
195
+ return data;
196
+ });
197
+ }
198
+ promise = promise.then(() =>
199
+ extractAndLoadSourceMaps(locationKeyToHookSourceData),
200
+ );
201
+ if (__PERFORMANCE_PROFILE__) {
202
+ promise = promise.then(data => {
203
+ mark('parseSourceAST()');
204
+ measure('extractAndLoadSourceMaps()');
205
+ return data;
206
+ });
207
+ }
208
+ promise = promise.then(() => parseSourceAST(locationKeyToHookSourceData));
209
+ if (__PERFORMANCE_PROFILE__) {
210
+ promise = promise.then(data => {
211
+ mark('updateLruCache()');
212
+ measure('parseSourceAST()');
213
+ return data;
214
+ });
215
+ }
216
+ promise = promise.then(() => updateLruCache(locationKeyToHookSourceData));
217
+ if (__PERFORMANCE_PROFILE__) {
218
+ promise = promise.then(data => {
219
+ mark('findHookNames()');
220
+ measure('updateLruCache()');
221
+ return data;
222
+ });
223
+ }
224
+ promise = promise.then(() =>
225
+ findHookNames(hooksList, locationKeyToHookSourceData),
226
+ );
227
+ if (__PERFORMANCE_PROFILE__) {
228
+ promise = promise.then(data => {
229
+ measure('findHookNames()');
230
+ measure('parseHookNames()');
231
+ return data;
232
+ });
233
+ }
234
+
235
+ return promise;
236
}
237
238
function decodeBase64String(encoded: string): Object {
@@ -214,7 +278,15 @@ function extractAndLoadSourceMaps(
278
279
const sourceMapRegex = / ?sourceMappingURL=([^\s'"]+)/gm;
280
const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
281
+
282
+ if (__PERFORMANCE_PROFILE__) {
283
+ mark('sourceMapRegex.exec(runtimeSourceCode)');
284
+ }
285
let sourceMappingURLMatch = sourceMapRegex.exec(runtimeSourceCode);
286
+ if (__PERFORMANCE_PROFILE__) {
287
+ measure('sourceMapRegex.exec(runtimeSourceCode)');
288
+ }
289
+
290
if (sourceMappingURLMatch == null) {
291
// Maybe file has not been transformed; we'll try to parse it as-is in parseSourceAST().
292
@@ -237,8 +309,21 @@ function extractAndLoadSourceMaps(
309
const trimmed = ((sourceMappingURL.match(
310
/base64,([a-zA-Z0-9+\/=]+)/,
311
): any): Array<string>)[1];
312
+ if (__PERFORMANCE_PROFILE__) {
313
+ mark('decodeBase64String()');
314
+ }
315
const decoded = decodeBase64String(trimmed);
316
+ if (__PERFORMANCE_PROFILE__) {
317
+ measure('decodeBase64String()');
318
+ }
319
+
320
+ if (__PERFORMANCE_PROFILE__) {
321
+ mark('JSON.parse(decoded)');
322
+ }
323
const parsed = JSON.parse(decoded);
324
+ if (__PERFORMANCE_PROFILE__) {
325
+ measure('JSON.parse(decoded)');
326
+ }
327
328
if (__DEBUG__) {
329
console.groupCollapsed(
@@ -251,10 +336,20 @@ function extractAndLoadSourceMaps(
336
// Hook source might be a URL like "https://4syus.csb.app/src/App.js"
337
// Parsed source map might be a partial path like "src/App.js"
338
if (sourceMapIncludesSource(parsed, runtimeSourceURL)) {
339
+ if (__PERFORMANCE_PROFILE__) {
340
+ mark('new SourceMapMetadataConsumer(parsed)');
341
+ }
342
hookSourceData.metadataConsumer = new SourceMapMetadataConsumer(
343
parsed,
344
);
345
+ if (__PERFORMANCE_PROFILE__) {
346
+ measure('new SourceMapMetadataConsumer(parsed)');
347
+ mark('new SourceMapConsumer(parsed)');
348
+ }
349
hookSourceData.sourceConsumer = new SourceMapConsumer(parsed);
350
+ if (__PERFORMANCE_PROFILE__) {
351
+ measure('new SourceMapConsumer(parsed)');
352
+ }
353
break;
354
}
355
} else {
@@ -300,11 +395,31 @@ function extractAndLoadSourceMaps(
395
fetchPromises.get(url) ||
396
fetchFile(url).then(
397
sourceMapContents => {
398
+ if (__PERFORMANCE_PROFILE__) {
399
+ mark('JSON.parse(sourceMapContents)');
400
+ }
401
const parsed = JSON.parse(sourceMapContents);
304
- return {
305
- sourceConsumer: new SourceMapConsumer(parsed),
306
- metadataConsumer: new SourceMapMetadataConsumer(parsed),
307
- };
402
+ if (__PERFORMANCE_PROFILE__) {
403
+ measure('JSON.parse(sourceMapContents)');
404
+ }
405
+
406
+ if (__PERFORMANCE_PROFILE__) {
407
+ mark('SourceMapConsumer(parsed)');
408
+ }
409
+ const sourceConsumer = new SourceMapConsumer(parsed);
410
+ if (__PERFORMANCE_PROFILE__) {
411
+ measure('SourceMapConsumer(parsed)');
412
+ }
413
+
414
+ if (__PERFORMANCE_PROFILE__) {
415
+ mark('SourceMapMetadataConsumer(parsed)');
416
+ }
417
+ const metadataConsumer = new SourceMapMetadataConsumer(parsed);
418
+ if (__PERFORMANCE_PROFILE__) {
419
+ measure('SourceMapMetadataConsumer(parsed)');
420
+ }
421
+
422
+ return {sourceConsumer, metadataConsumer};
423
},
424
// In this case, we fall back to the assumption that the source has no source map.
425
// This might indicate an (unlikely) edge case that had no source map,
@@ -404,6 +519,9 @@ function findHookNames(
519
originalSourceColumnNumber = columnNumber;
520
originalSourceLineNumber = lineNumber;
521
} else {
522
+ if (__PERFORMANCE_PROFILE__) {
523
+ mark('sourceConsumer.originalPositionFor()');
524
+ }
525
const position = sourceConsumer.originalPositionFor({
526
line: lineNumber,
527
@@ -412,6 +530,9 @@ function findHookNames(
530
// For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
531
column: columnNumber - 1,
532
});
533
+ if (__PERFORMANCE_PROFILE__) {
534
+ measure('sourceConsumer.originalPositionFor()');
535
+ }
536
537
originalSourceColumnNumber = position.column;
538
originalSourceLineNumber = position.line;
@@ -434,14 +555,23 @@ function findHookNames(
555
let name;
556
const {metadataConsumer} = hookSourceData;
557
if (metadataConsumer != null) {
558
+ if (__PERFORMANCE_PROFILE__) {
559
+ mark('metadataConsumer.hookNameFor()');
560
+ }
561
name = metadataConsumer.hookNameFor({
562
line: originalSourceLineNumber,
563
column: originalSourceColumnNumber,
564
source: originalSourceURL,
565
});
566
+ if (__PERFORMANCE_PROFILE__) {
567
+ measure('metadataConsumer.hookNameFor()');
568
+ }
569
}
570
571
if (name == null) {
572
+ if (__PERFORMANCE_PROFILE__) {
573
+ mark('getHookName()');
574
+ }
575
name = getHookName(
576
hook,
577
hookSourceData.originalSourceAST,
@@ -449,6 +579,9 @@ function findHookNames(
579
((originalSourceLineNumber: any): number),
580
originalSourceColumnNumber,
581
);
582
+ if (__PERFORMANCE_PROFILE__) {
583
+ measure('getHookName()');
584
+ }
585
}
586
587
if (__DEBUG__) {
@@ -516,6 +649,9 @@ async function parseSourceAST(
649
if (lineNumber == null || columnNumber == null) {
650
throw Error('Hook source code location not found.');
651
}
652
+ if (__PERFORMANCE_PROFILE__) {
653
+ mark('sourceConsumer.originalPositionFor()');
654
+ }
655
// Now that the source map has been loaded,
656
// extract the original source for later.
657
const {source} = sourceConsumer.originalPositionFor({
@@ -526,6 +662,9 @@ async function parseSourceAST(
662
// For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
663
column: columnNumber - 1,
664
});
665
+ if (__PERFORMANCE_PROFILE__) {
666
+ measure('sourceConsumer.originalPositionFor()');
667
+ }
668
669
if (source == null) {
670
// TODO (named hooks) maybe fall back to the runtime source instead of throwing?
@@ -538,10 +677,16 @@ async function parseSourceAST(
677
// It can be relative if the source map specifies it that way,
678
// but we use it as a cache key across different source maps and there can be collisions.
679
originalSourceURL = (source: string);
680
+ if (__PERFORMANCE_PROFILE__) {
681
+ mark('sourceConsumer.sourceContentFor()');
682
+ }
683
originalSourceCode = (sourceConsumer.sourceContentFor(
684
source,
685
true,
686
): string);
687
+ if (__PERFORMANCE_PROFILE__) {
688
+ measure('sourceConsumer.sourceContentFor()');
689
+ }
690
691
if (__DEBUG__) {
692
console.groupCollapsed(
@@ -595,10 +740,16 @@ async function parseSourceAST(
740
originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript';
741
742
// TODO (named hooks) Parsing should ideally be done off of the main thread.
743
+ if (__PERFORMANCE_PROFILE__) {
744
+ mark('[@babel/parser] parse(originalSourceCode)');
745
+ }
746
const originalSourceAST = parse(originalSourceCode, {
747
sourceType: 'unambiguous',
748
plugins: ['jsx', plugin],
749
});
750
+ if (__PERFORMANCE_PROFILE__) {
751
+ measure('[@babel/parser] parse(originalSourceCode)');
752
+ }
753
hookSourceData.originalSourceAST = originalSourceAST;
754
if (__DEBUG__) {
755
console.log(
packages/react-devtools-shared/src/constants.js
+3
@@ -10,6 +10,9 @@
10
// Flip this flag to true to enable verbose console debug logging.
11
export const __DEBUG__ = false;
12
13
+// Flip this flag to true to enable performance.mark() and performance.measure() timings.
14
+export const __PERFORMANCE_PROFILE__ = false;
15
+
16
export const TREE_OPERATION_ADD = 1;
17
export const TREE_OPERATION_REMOVE = 2;
18
export const TREE_OPERATION_REORDER_CHILDREN = 3;