@samitouri / QOS-React-2 / commits / 8456457c8d

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;