Added performance timings to DevTools named hooks parsing (#22173)

This commit is contained in:
Brian Vaughn 2021-08-25 15:39:15 -04:00 committed by GitHub
parent b9964684bd
commit 8456457c8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 203 additions and 11 deletions

View File

@ -7,6 +7,7 @@
* @flow
*/
import {__PERFORMANCE_PROFILE__} from 'react-devtools-shared/src/constants';
import traverse, {NodePath, Node} from '@babel/traverse';
import {File} from '@babel/types';
@ -27,6 +28,15 @@ export type SourceFileASTWithHookDetails = {
export const NO_HOOK_NAME = '<no-hook>';
function mark(markName: string): void {
performance.mark(markName + '-start');
}
function measure(markName: string): void {
performance.mark(markName + '-end');
performance.measure(markName, markName + '-start', markName + '-end');
}
const AST_NODE_TYPES = Object.freeze({
PROGRAM: 'Program',
CALL_EXPRESSION: 'CallExpression',
@ -131,7 +141,13 @@ export function getHookName(
originalSourceLineNumber: number,
originalSourceColumnNumber: number,
): string | null {
if (__PERFORMANCE_PROFILE__) {
mark('getPotentialHookDeclarationsFromAST(originalSourceAST)');
}
const hooksFromAST = getPotentialHookDeclarationsFromAST(originalSourceAST);
if (__PERFORMANCE_PROFILE__) {
measure('getPotentialHookDeclarationsFromAST(originalSourceAST)');
}
let potentialReactHookASTNode = null;
if (originalSourceColumnNumber === 0) {
@ -144,6 +160,7 @@ export function getHookName(
node,
originalSourceLineNumber,
);
const hookDeclaractionCheck = isConfirmedHookDeclaration(node);
return nodeLocationCheck && hookDeclaractionCheck;
});
@ -158,6 +175,7 @@ export function getHookName(
originalSourceLineNumber,
originalSourceColumnNumber,
);
const hookDeclaractionCheck = isConfirmedHookDeclaration(node);
return nodeLocationCheck && hookDeclaractionCheck;
});
@ -170,17 +188,31 @@ export function getHookName(
// nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
// depending on the type of potentialReactHookASTNode
try {
if (__PERFORMANCE_PROFILE__) {
mark('getFilteredHookASTNodes()');
}
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hooksFromAST,
originalSourceCode,
);
if (__PERFORMANCE_PROFILE__) {
measure('getFilteredHookASTNodes()');
}
return getHookNameFromNode(
if (__PERFORMANCE_PROFILE__) {
mark('getHookNameFromNode()');
}
const name = getHookNameFromNode(
hook,
nodesAssociatedWithReactHookASTNode,
potentialReactHookASTNode,
);
if (__PERFORMANCE_PROFILE__) {
measure('getHookNameFromNode()');
}
return name;
} catch (error) {
console.error(error);
return null;
@ -283,6 +315,9 @@ function getHookVariableName(
function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
const potentialHooksFound: NodePath[] = [];
if (__PERFORMANCE_PROFILE__) {
mark('traverse(sourceAST)');
}
traverse(sourceAST, {
enter(path) {
if (path.isVariableDeclarator() && isPotentialHookDeclaration(path)) {
@ -290,6 +325,9 @@ function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
}
},
});
if (__PERFORMANCE_PROFILE__) {
measure('traverse(sourceAST)');
}
return potentialHooksFound;
}

View File

@ -12,7 +12,10 @@ import LRU from 'lru-cache';
import {SourceMapConsumer} from 'source-map-js';
import {getHookName} from '../astUtils';
import {areSourceMapsAppliedToErrors} from '../ErrorTester';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {
__DEBUG__,
__PERFORMANCE_PROFILE__,
} from 'react-devtools-shared/src/constants';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {sourceMapIncludesSource} from '../SourceMapUtils';
import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
@ -26,6 +29,15 @@ import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
import type {Thenable} from 'shared/ReactTypes';
import type {SourceConsumer} from '../astUtils';
function mark(markName: string): void {
performance.mark(markName + '-start');
}
function measure(markName: string): void {
performance.mark(markName + '-end');
performance.measure(markName, markName + '-start', markName + '-end');
}
const MAX_SOURCE_LENGTH = 100_000_000;
type AST = mixed;
@ -107,8 +119,15 @@ const originalURLToMetadataCache: LRUCache<
export async function parseHookNames(
hooksTree: HooksTree,
): Thenable<HookNames | null> {
if (__PERFORMANCE_PROFILE__) {
mark('parseHookNames()');
mark('flattenHooksList()');
}
const hooksList: Array<HooksNode> = [];
flattenHooksList(hooksTree, hooksList);
if (__PERFORMANCE_PROFILE__) {
measure('flattenHooksList()');
}
if (__DEBUG__) {
console.log('parseHookNames() hooksList:', hooksList);
@ -164,11 +183,56 @@ export async function parseHookNames(
}
}
return loadSourceFiles(locationKeyToHookSourceData)
.then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData))
.then(() => parseSourceAST(locationKeyToHookSourceData))
.then(() => updateLruCache(locationKeyToHookSourceData))
.then(() => findHookNames(hooksList, locationKeyToHookSourceData));
if (__PERFORMANCE_PROFILE__) {
mark('loadSourceFiles()');
}
let promise = loadSourceFiles(locationKeyToHookSourceData);
if (__PERFORMANCE_PROFILE__) {
promise = promise.then(data => {
mark('extractAndLoadSourceMaps()');
measure('loadSourceFiles()');
return data;
});
}
promise = promise.then(() =>
extractAndLoadSourceMaps(locationKeyToHookSourceData),
);
if (__PERFORMANCE_PROFILE__) {
promise = promise.then(data => {
mark('parseSourceAST()');
measure('extractAndLoadSourceMaps()');
return data;
});
}
promise = promise.then(() => parseSourceAST(locationKeyToHookSourceData));
if (__PERFORMANCE_PROFILE__) {
promise = promise.then(data => {
mark('updateLruCache()');
measure('parseSourceAST()');
return data;
});
}
promise = promise.then(() => updateLruCache(locationKeyToHookSourceData));
if (__PERFORMANCE_PROFILE__) {
promise = promise.then(data => {
mark('findHookNames()');
measure('updateLruCache()');
return data;
});
}
promise = promise.then(() =>
findHookNames(hooksList, locationKeyToHookSourceData),
);
if (__PERFORMANCE_PROFILE__) {
promise = promise.then(data => {
measure('findHookNames()');
measure('parseHookNames()');
return data;
});
}
return promise;
}
function decodeBase64String(encoded: string): Object {
@ -214,7 +278,15 @@ function extractAndLoadSourceMaps(
const sourceMapRegex = / ?sourceMappingURL=([^\s'"]+)/gm;
const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
if (__PERFORMANCE_PROFILE__) {
mark('sourceMapRegex.exec(runtimeSourceCode)');
}
let sourceMappingURLMatch = sourceMapRegex.exec(runtimeSourceCode);
if (__PERFORMANCE_PROFILE__) {
measure('sourceMapRegex.exec(runtimeSourceCode)');
}
if (sourceMappingURLMatch == null) {
// Maybe file has not been transformed; we'll try to parse it as-is in parseSourceAST().
@ -237,8 +309,21 @@ function extractAndLoadSourceMaps(
const trimmed = ((sourceMappingURL.match(
/base64,([a-zA-Z0-9+\/=]+)/,
): any): Array<string>)[1];
if (__PERFORMANCE_PROFILE__) {
mark('decodeBase64String()');
}
const decoded = decodeBase64String(trimmed);
if (__PERFORMANCE_PROFILE__) {
measure('decodeBase64String()');
}
if (__PERFORMANCE_PROFILE__) {
mark('JSON.parse(decoded)');
}
const parsed = JSON.parse(decoded);
if (__PERFORMANCE_PROFILE__) {
measure('JSON.parse(decoded)');
}
if (__DEBUG__) {
console.groupCollapsed(
@ -251,10 +336,20 @@ function extractAndLoadSourceMaps(
// Hook source might be a URL like "https://4syus.csb.app/src/App.js"
// Parsed source map might be a partial path like "src/App.js"
if (sourceMapIncludesSource(parsed, runtimeSourceURL)) {
if (__PERFORMANCE_PROFILE__) {
mark('new SourceMapMetadataConsumer(parsed)');
}
hookSourceData.metadataConsumer = new SourceMapMetadataConsumer(
parsed,
);
if (__PERFORMANCE_PROFILE__) {
measure('new SourceMapMetadataConsumer(parsed)');
mark('new SourceMapConsumer(parsed)');
}
hookSourceData.sourceConsumer = new SourceMapConsumer(parsed);
if (__PERFORMANCE_PROFILE__) {
measure('new SourceMapConsumer(parsed)');
}
break;
}
} else {
@ -300,11 +395,31 @@ function extractAndLoadSourceMaps(
fetchPromises.get(url) ||
fetchFile(url).then(
sourceMapContents => {
if (__PERFORMANCE_PROFILE__) {
mark('JSON.parse(sourceMapContents)');
}
const parsed = JSON.parse(sourceMapContents);
return {
sourceConsumer: new SourceMapConsumer(parsed),
metadataConsumer: new SourceMapMetadataConsumer(parsed),
};
if (__PERFORMANCE_PROFILE__) {
measure('JSON.parse(sourceMapContents)');
}
if (__PERFORMANCE_PROFILE__) {
mark('SourceMapConsumer(parsed)');
}
const sourceConsumer = new SourceMapConsumer(parsed);
if (__PERFORMANCE_PROFILE__) {
measure('SourceMapConsumer(parsed)');
}
if (__PERFORMANCE_PROFILE__) {
mark('SourceMapMetadataConsumer(parsed)');
}
const metadataConsumer = new SourceMapMetadataConsumer(parsed);
if (__PERFORMANCE_PROFILE__) {
measure('SourceMapMetadataConsumer(parsed)');
}
return {sourceConsumer, metadataConsumer};
},
// In this case, we fall back to the assumption that the source has no source map.
// This might indicate an (unlikely) edge case that had no source map,
@ -404,6 +519,9 @@ function findHookNames(
originalSourceColumnNumber = columnNumber;
originalSourceLineNumber = lineNumber;
} else {
if (__PERFORMANCE_PROFILE__) {
mark('sourceConsumer.originalPositionFor()');
}
const position = sourceConsumer.originalPositionFor({
line: lineNumber,
@ -412,6 +530,9 @@ function findHookNames(
// For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
column: columnNumber - 1,
});
if (__PERFORMANCE_PROFILE__) {
measure('sourceConsumer.originalPositionFor()');
}
originalSourceColumnNumber = position.column;
originalSourceLineNumber = position.line;
@ -434,14 +555,23 @@ function findHookNames(
let name;
const {metadataConsumer} = hookSourceData;
if (metadataConsumer != null) {
if (__PERFORMANCE_PROFILE__) {
mark('metadataConsumer.hookNameFor()');
}
name = metadataConsumer.hookNameFor({
line: originalSourceLineNumber,
column: originalSourceColumnNumber,
source: originalSourceURL,
});
if (__PERFORMANCE_PROFILE__) {
measure('metadataConsumer.hookNameFor()');
}
}
if (name == null) {
if (__PERFORMANCE_PROFILE__) {
mark('getHookName()');
}
name = getHookName(
hook,
hookSourceData.originalSourceAST,
@ -449,6 +579,9 @@ function findHookNames(
((originalSourceLineNumber: any): number),
originalSourceColumnNumber,
);
if (__PERFORMANCE_PROFILE__) {
measure('getHookName()');
}
}
if (__DEBUG__) {
@ -516,6 +649,9 @@ async function parseSourceAST(
if (lineNumber == null || columnNumber == null) {
throw Error('Hook source code location not found.');
}
if (__PERFORMANCE_PROFILE__) {
mark('sourceConsumer.originalPositionFor()');
}
// Now that the source map has been loaded,
// extract the original source for later.
const {source} = sourceConsumer.originalPositionFor({
@ -526,6 +662,9 @@ async function parseSourceAST(
// For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
column: columnNumber - 1,
});
if (__PERFORMANCE_PROFILE__) {
measure('sourceConsumer.originalPositionFor()');
}
if (source == null) {
// TODO (named hooks) maybe fall back to the runtime source instead of throwing?
@ -538,10 +677,16 @@ async function parseSourceAST(
// It can be relative if the source map specifies it that way,
// but we use it as a cache key across different source maps and there can be collisions.
originalSourceURL = (source: string);
if (__PERFORMANCE_PROFILE__) {
mark('sourceConsumer.sourceContentFor()');
}
originalSourceCode = (sourceConsumer.sourceContentFor(
source,
true,
): string);
if (__PERFORMANCE_PROFILE__) {
measure('sourceConsumer.sourceContentFor()');
}
if (__DEBUG__) {
console.groupCollapsed(
@ -595,10 +740,16 @@ async function parseSourceAST(
originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript';
// TODO (named hooks) Parsing should ideally be done off of the main thread.
if (__PERFORMANCE_PROFILE__) {
mark('[@babel/parser] parse(originalSourceCode)');
}
const originalSourceAST = parse(originalSourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx', plugin],
});
if (__PERFORMANCE_PROFILE__) {
measure('[@babel/parser] parse(originalSourceCode)');
}
hookSourceData.originalSourceAST = originalSourceAST;
if (__DEBUG__) {
console.log(

View File

@ -10,6 +10,9 @@
// Flip this flag to true to enable verbose console debug logging.
export const __DEBUG__ = false;
// Flip this flag to true to enable performance.mark() and performance.measure() timings.
export const __PERFORMANCE_PROFILE__ = false;
export const TREE_OPERATION_ADD = 1;
export const TREE_OPERATION_REMOVE = 2;
export const TREE_OPERATION_REORDER_CHILDREN = 3;