From 415cdd611d916596160e21d33fe6bfa50774933c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 22 Sep 2021 18:53:19 -0400 Subject: [PATCH 1/3] Added sourcemap-codec implementation for parsing source-maps --- .../src/hooks/SourceMapConsumer.js | 241 ++++++++++++++++ .../parseHookNames/parseSourceAndMetadata.js | 261 +++++++++++++++++- 2 files changed, 488 insertions(+), 14 deletions(-) create mode 100644 packages/react-devtools-shared/src/hooks/SourceMapConsumer.js diff --git a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js new file mode 100644 index 0000000000000..f0503ef91a237 --- /dev/null +++ b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js @@ -0,0 +1,241 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils'; +import {decode} from 'sourcemap-codec'; + +import type { + IndexSourceMap, + BasicSourceMap, + MixedSourceMap, +} from './SourceMapTypes'; + +type SearchPosition = {| + columnNumber: number, + lineNumber: number, +|}; + +type ResultPosition = {| + column: number, + line: number, + sourceContent: string, + sourceURL: string, +|}; + +export type SourceMapConsumerType = {| + originalPositionFor: SearchPosition => ResultPosition, +|}; + +type Mappings = Array>>; + +export default function SourceMapConsumer( + sourceMapJSON: MixedSourceMap, +): SourceMapConsumerType { + if (sourceMapJSON.sections != null) { + return IndexedSourceMapConsumer(((sourceMapJSON: any): IndexSourceMap)); + } else { + return BasicSourceMapConsumer(((sourceMapJSON: any): BasicSourceMap)); + } +} + +function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) { + const decodedMappings: Mappings = withSyncPerfMeasurements( + 'Decoding source map mappings with sourcemap-codec', + () => decode(sourceMapJSON.mappings), + ); + + function originalPositionFor({ + columnNumber, + lineNumber, + }: SearchPosition): ResultPosition { + // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based. + const targetColumnNumber = columnNumber - 1; + + const lineMappings = decodedMappings[lineNumber - 1]; + + let nearestEntry = null; + + let startIndex = 0; + let stopIndex = lineMappings.length - 1; + let index = -1; + while (startIndex <= stopIndex) { + index = Math.floor((stopIndex + startIndex) / 2); + nearestEntry = lineMappings[index]; + + const currentColumn = nearestEntry[0]; + if (currentColumn === targetColumnNumber) { + break; + } else { + if (currentColumn > targetColumnNumber) { + if (stopIndex - index > 0) { + stopIndex = index; + } else { + index = stopIndex; + break; + } + } else { + if (index - startIndex > 0) { + startIndex = index; + } else { + index = startIndex; + break; + } + } + } + } + + // We have found either the exact element, or the next-closest element. + // However there may be more than one such element. + // Make sure we always return the smallest of these. + while (index > 0) { + const previousEntry = lineMappings[index - 1]; + const currentColumn = previousEntry[0]; + if (currentColumn !== targetColumnNumber) { + break; + } + index--; + } + + if (nearestEntry == null) { + // TODO maybe fall back to the runtime source instead of throwing? + throw Error( + `Could not find runtime location for line:${lineNumber} and column:${columnNumber}`, + ); + } + + const sourceIndex = nearestEntry[1]; + const sourceContent = + sourceMapJSON.sourcesContent != null + ? sourceMapJSON.sourcesContent[sourceIndex] + : null; + const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null; + const line = nearestEntry[2] + 1; + const column = nearestEntry[3]; + + if (sourceContent === null || sourceURL === null) { + // TODO maybe fall back to the runtime source instead of throwing? + throw Error( + `Could not find original source for line:${lineNumber} and column:${columnNumber}`, + ); + } + + return { + column, + line, + sourceContent: ((sourceContent: any): string), + sourceURL: ((sourceURL: any): string), + }; + } + + return (({ + originalPositionFor, + }: any): SourceMapConsumerType); +} + +function IndexedSourceMapConsumer(sourceMapJSON: IndexSourceMap) { + let lastOffset = { + line: -1, + column: 0, + }; + + const sections = sourceMapJSON.sections.map(section => { + const offset = section.offset; + const offsetLine = offset.line; + const offsetColumn = offset.column; + + if ( + offsetLine < lastOffset.line || + (offsetLine === lastOffset.line && offsetColumn < lastOffset.column) + ) { + throw new Error('Section offsets must be ordered and non-overlapping.'); + } + + lastOffset = offset; + + return { + // The offset fields are 0-based, but we use 1-based indices when encoding/decoding from VLQ. + generatedLine: offsetLine + 1, + generatedColumn: offsetColumn + 1, + sourceMapConsumer: new SourceMapConsumer(section.map), + }; + }); + + function originalPositionFor({ + columnNumber, + lineNumber, + }: SearchPosition): ResultPosition { + // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based. + const targetColumnNumber = columnNumber - 1; + + let section = null; + + let startIndex = 0; + let stopIndex = sections.length - 1; + let index = -1; + while (startIndex <= stopIndex) { + index = Math.floor((stopIndex + startIndex) / 2); + section = sections[index]; + + const currentLine = section.generatedLine; + if (currentLine === lineNumber) { + const currentColumn = section.generatedColumn; + if (currentColumn === lineNumber) { + break; + } else { + if (currentColumn > targetColumnNumber) { + if (stopIndex - index > 0) { + stopIndex = index; + } else { + index = stopIndex; + break; + } + } else { + if (index - startIndex > 0) { + startIndex = index; + } else { + index = startIndex; + break; + } + } + } + } else { + if (currentLine > lineNumber) { + if (stopIndex - index > 0) { + stopIndex = index; + } else { + index = stopIndex; + break; + } + } else { + if (index - startIndex > 0) { + startIndex = index; + } else { + index = startIndex; + break; + } + } + } + } + + if (section == null) { + // TODO maybe fall back to the runtime source instead of throwing? + throw Error( + `Could not find matching section for line:${lineNumber} and column:${columnNumber}`, + ); + } + + return section.sourceMapConsumer.originalPositionFor({ + columnNumber, + lineNumber, + }); + } + + return (({ + originalPositionFor, + }: any): SourceMapConsumerType); +} diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 6f87b351a014a..ab6c8f94b6cfe 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -22,7 +22,9 @@ import { withAsyncPerfMeasurements, withSyncPerfMeasurements, } from 'react-devtools-shared/src/PerformanceLoggingUtils'; +import SourceMapCodecConsumer from '../SourceMapConsumer'; +import type {SourceMapConsumerType} from '../SourceMapConsumer'; import type { HooksList, LocationKeyToHookSourceAndMetadata, @@ -31,6 +33,8 @@ import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; import type {HookNames, LRUCache} from 'react-devtools-shared/src/types'; import type {SourceConsumer} from '../astUtils'; +const USE_ALTERNATE_SOURCE_MAP = true; + type AST = mixed; type HookParsedMetadata = {| @@ -55,13 +59,17 @@ type HookParsedMetadata = {| // APIs from source-map for parsing source maps (if detected). sourceConsumer: SourceConsumer | null, + + // Alternate APIs from source-map for parsing source maps (if detected). + sourceMapConsumer: SourceMapConsumerType | null, |}; type LocationKeyToHookParsedMetadata = Map; type CachedRuntimeCodeMetadata = {| - sourceConsumer: SourceConsumer | null, metadataConsumer: SourceMapMetadataConsumer | null, + sourceConsumer: SourceConsumer | null, + sourceMapConsumer: SourceMapConsumerType | null, |}; const runtimeURLToMetadataCache: LRUCache< @@ -112,19 +120,35 @@ export async function parseSourceAndMetadata( () => initializeHookParsedMetadata(locationKeyToHookSourceAndMetadata), ); - withSyncPerfMeasurements('parseSourceMaps', () => - parseSourceMaps( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); + if (USE_ALTERNATE_SOURCE_MAP) { + withSyncPerfMeasurements('parseSourceMapsAlternate', () => + parseSourceMapsAlternate( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); - withSyncPerfMeasurements('parseSourceAST()', () => - parseSourceAST( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); + withSyncPerfMeasurements('parseSourceASTAlternate()', () => + parseSourceASTAlternate( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); + } else { + withSyncPerfMeasurements('parseSourceMaps', () => + parseSourceMaps( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); + + withSyncPerfMeasurements('parseSourceAST()', () => + parseSourceAST( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); + } return withSyncPerfMeasurements('findHookNames()', () => findHookNames(hooksList, locationKeyToHookParsedMetadata), @@ -221,6 +245,7 @@ function initializeHookParsedMetadata( originalSourceLineNumber: null, originalSourceColumnNumber: null, sourceConsumer: null, + sourceMapConsumer: null, }; locationKeyToHookParsedMetadata.set(locationKey, hookParsedMetadata); @@ -238,8 +263,10 @@ function initializeHookParsedMetadata( console.log(runtimeMetadata); console.groupEnd(); } - hookParsedMetadata.sourceConsumer = runtimeMetadata.sourceConsumer; hookParsedMetadata.metadataConsumer = runtimeMetadata.metadataConsumer; + hookParsedMetadata.sourceConsumer = runtimeMetadata.sourceConsumer; + hookParsedMetadata.sourceMapConsumer = + runtimeMetadata.sourceMapConsumer; } }, ); @@ -282,6 +309,7 @@ function parseSourceAST( const {metadataConsumer, sourceConsumer} = hookParsedMetadata; const runtimeSourceCode = ((hookSourceAndMetadata.runtimeSourceCode: any): string); + let hasHookMap = false; let originalSourceURL; let originalSourceCode; @@ -434,6 +462,165 @@ function parseSourceAST( ); } +function parseSourceASTAlternate( + locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata, +): void { + locationKeyToHookSourceAndMetadata.forEach( + (hookSourceAndMetadata, locationKey) => { + const hookParsedMetadata = locationKeyToHookParsedMetadata.get( + locationKey, + ); + if (hookParsedMetadata == null) { + throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); + } + + if (hookParsedMetadata.originalSourceAST !== null) { + // Use cached metadata. + return; + } + + if ( + hookParsedMetadata.originalSourceURL != null && + hookParsedMetadata.originalSourceCode != null && + hookParsedMetadata.originalSourceColumnNumber != null && + hookParsedMetadata.originalSourceLineNumber != null + ) { + // Use cached metadata. + return; + } + + const {lineNumber, columnNumber} = hookSourceAndMetadata.hookSource; + if (lineNumber == null || columnNumber == null) { + throw Error('Hook source code location not found.'); + } + + const {metadataConsumer, sourceMapConsumer} = hookParsedMetadata; + const runtimeSourceCode = ((hookSourceAndMetadata.runtimeSourceCode: any): string); + let hasHookMap = false; + let originalSourceURL; + let originalSourceCode; + let originalSourceColumnNumber; + let originalSourceLineNumber; + if (areSourceMapsAppliedToErrors() || sourceMapConsumer === null) { + // Either the current environment automatically applies source maps to errors, + // or the current code had no source map to begin with. + // Either way, we don't need to convert the Error stack frame locations. + originalSourceColumnNumber = columnNumber; + originalSourceLineNumber = lineNumber; + // There's no source map to parse here so we can just parse the original source itself. + originalSourceCode = runtimeSourceCode; + // TODO (named hooks) This mixes runtimeSourceURLs with source mapped URLs in the same cache key space. + // Namespace them? + originalSourceURL = hookSourceAndMetadata.runtimeSourceURL; + } else { + const { + column, + line, + sourceContent, + sourceURL, + } = sourceMapConsumer.originalPositionFor({ + columnNumber, + lineNumber, + }); + + originalSourceColumnNumber = column; + originalSourceLineNumber = line; + originalSourceCode = sourceContent; + originalSourceURL = sourceURL; + } + + hookParsedMetadata.originalSourceCode = originalSourceCode; + hookParsedMetadata.originalSourceURL = originalSourceURL; + hookParsedMetadata.originalSourceLineNumber = originalSourceLineNumber; + hookParsedMetadata.originalSourceColumnNumber = originalSourceColumnNumber; + + if ( + metadataConsumer != null && + metadataConsumer.hasHookMap(originalSourceURL) + ) { + hasHookMap = true; + } + + if (__DEBUG__) { + console.log( + `parseSourceAST() mapped line ${lineNumber}->${originalSourceLineNumber} and column ${columnNumber}->${originalSourceColumnNumber}`, + ); + } + + if (hasHookMap) { + if (__DEBUG__) { + console.log( + `parseSourceAST() Found hookMap and skipping parsing for "${originalSourceURL}"`, + ); + } + // If there's a hook map present from an extended sourcemap then + // we don't need to parse the source files and instead can use the + // hook map to extract hook names. + return; + } + + if (__DEBUG__) { + console.log( + `parseSourceAST() Did not find hook map for "${originalSourceURL}"`, + ); + } + + // The cache also serves to deduplicate parsing by URL in our loop over location keys. + // This may need to change if we switch to async parsing. + const sourceMetadata = originalURLToMetadataCache.get(originalSourceURL); + if (sourceMetadata != null) { + if (__DEBUG__) { + console.groupCollapsed( + `parseSourceAST() Found cached source metadata for "${originalSourceURL}"`, + ); + console.log(sourceMetadata); + console.groupEnd(); + } + hookParsedMetadata.originalSourceAST = sourceMetadata.originalSourceAST; + hookParsedMetadata.originalSourceCode = + sourceMetadata.originalSourceCode; + } else { + try { + // TypeScript is the most commonly used typed JS variant so let's default to it + // unless we detect explicit Flow usage via the "@flow" pragma. + const plugin = + originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript'; + + // TODO (named hooks) This is probably where we should check max source length, + // rather than in loadSourceAndMetatada -> loadSourceFiles(). + // TODO(#22319): Support source files that are html files with inline script tags. + const originalSourceAST = withSyncPerfMeasurements( + '[@babel/parser] parse(originalSourceCode)', + () => + parse(originalSourceCode, { + sourceType: 'unambiguous', + plugins: ['jsx', plugin], + }), + ); + hookParsedMetadata.originalSourceAST = originalSourceAST; + + if (__DEBUG__) { + console.log( + `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, + ); + } + + originalURLToMetadataCache.set(originalSourceURL, { + originalSourceAST, + originalSourceCode, + }); + } catch (error) { + throw new Error( + `Failed to parse source file: ${originalSourceURL}\n\n` + + `Original error: ${error}`, + ); + } + } + }, + ); +} + function parseSourceMaps( locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata, locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata, @@ -471,6 +658,52 @@ function parseSourceMaps( runtimeURLToMetadataCache.set(runtimeSourceURL, { metadataConsumer: hookParsedMetadata.metadataConsumer, sourceConsumer: hookParsedMetadata.sourceConsumer, + sourceMapConsumer: null, + }); + } + } + }, + ); +} + +function parseSourceMapsAlternate( + locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata, +) { + locationKeyToHookSourceAndMetadata.forEach( + (hookSourceAndMetadata, locationKey) => { + const hookParsedMetadata = locationKeyToHookParsedMetadata.get( + locationKey, + ); + if (hookParsedMetadata == null) { + throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); + } + + const sourceMapJSON = hookSourceAndMetadata.sourceMapJSON; + if (sourceMapJSON != null) { + hookParsedMetadata.metadataConsumer = withSyncPerfMeasurements( + 'new SourceMapMetadataConsumer(sourceMapJSON)', + () => new SourceMapMetadataConsumer(sourceMapJSON), + ); + hookParsedMetadata.sourceMapConsumer = withSyncPerfMeasurements( + 'new SourceMapCodecConsumer(sourceMapJSON)', + () => SourceMapCodecConsumer(sourceMapJSON), + ); + + const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; + + // Only set once to avoid triggering eviction/cleanup code. + if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) { + if (__DEBUG__) { + console.log( + `parseSourceMaps() Caching runtime metadata for "${runtimeSourceURL}"`, + ); + } + + runtimeURLToMetadataCache.set(runtimeSourceURL, { + metadataConsumer: hookParsedMetadata.metadataConsumer, + sourceConsumer: null, + sourceMapConsumer: hookParsedMetadata.sourceMapConsumer, }); } } From aacf09144c7f94aa6252852c3eeeb544ddc5aefe Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 22 Sep 2021 18:53:45 -0400 Subject: [PATCH 2/3] Fixed cache miss that caused us to re-parse source-maps too often in some cases --- .../parseHookNames/parseSourceAndMetadata.js | 124 +++++++++--------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index ab6c8f94b6cfe..36eed8f589d14 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -84,6 +84,7 @@ const runtimeURLToMetadataCache: LRUCache< ); } +console.log(`runtimeURLToMetadataCache() dispose of "${runtimeSourceURL}"`); const sourceConsumer = metadata.sourceConsumer; if (sourceConsumer !== null) { sourceConsumer.destroy(); @@ -249,25 +250,6 @@ function initializeHookParsedMetadata( }; locationKeyToHookParsedMetadata.set(locationKey, hookParsedMetadata); - - const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; - - // If we've already loaded the source map info for this file, - // we can skip reloading it (and more importantly, re-parsing it). - const runtimeMetadata = runtimeURLToMetadataCache.get(runtimeSourceURL); - if (runtimeMetadata != null) { - if (__DEBUG__) { - console.groupCollapsed( - `parseHookNames() Found cached runtime metadata for file "${runtimeSourceURL}"`, - ); - console.log(runtimeMetadata); - console.groupEnd(); - } - hookParsedMetadata.metadataConsumer = runtimeMetadata.metadataConsumer; - hookParsedMetadata.sourceConsumer = runtimeMetadata.sourceConsumer; - hookParsedMetadata.sourceMapConsumer = - runtimeMetadata.sourceMapConsumer; - } }, ); @@ -635,32 +617,40 @@ function parseSourceMaps( } const sourceMapJSON = hookSourceAndMetadata.sourceMapJSON; - if (sourceMapJSON != null) { - hookParsedMetadata.metadataConsumer = withSyncPerfMeasurements( - 'new SourceMapMetadataConsumer(sourceMapJSON)', - () => new SourceMapMetadataConsumer(sourceMapJSON), - ); - hookParsedMetadata.sourceConsumer = withSyncPerfMeasurements( - 'new SourceMapConsumer(sourceMapJSON)', - () => new SourceMapConsumer(sourceMapJSON), - ); - const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; + if (hookParsedMetadata.sourceConsumer === null) { + if (sourceMapJSON != null) { + hookParsedMetadata.sourceConsumer = withSyncPerfMeasurements( + 'new SourceMapConsumer(sourceMapJSON)', + () => new SourceMapConsumer(sourceMapJSON), + ); + } + } - // Only set once to avoid triggering eviction/cleanup code. - if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) { - if (__DEBUG__) { - console.log( - `parseSourceMaps() Caching runtime metadata for "${runtimeSourceURL}"`, - ); - } + if (hookParsedMetadata.metadataConsumer === null) { + if (sourceMapJSON != null) { + hookParsedMetadata.metadataConsumer = withSyncPerfMeasurements( + 'new SourceMapMetadataConsumer(sourceMapJSON)', + () => new SourceMapMetadataConsumer(sourceMapJSON), + ); + } + } - runtimeURLToMetadataCache.set(runtimeSourceURL, { - metadataConsumer: hookParsedMetadata.metadataConsumer, - sourceConsumer: hookParsedMetadata.sourceConsumer, - sourceMapConsumer: null, - }); + const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; + + // Only set once to avoid triggering eviction/cleanup code. + if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) { + if (__DEBUG__) { + console.log( + `parseSourceMaps() Caching runtime metadata for "${runtimeSourceURL}"`, + ); } + + runtimeURLToMetadataCache.set(runtimeSourceURL, { + metadataConsumer: hookParsedMetadata.metadataConsumer, + sourceConsumer: hookParsedMetadata.sourceConsumer, + sourceMapConsumer: null, + }); } }, ); @@ -679,31 +669,43 @@ function parseSourceMapsAlternate( throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); } - const sourceMapJSON = hookSourceAndMetadata.sourceMapJSON; - if (sourceMapJSON != null) { - hookParsedMetadata.metadataConsumer = withSyncPerfMeasurements( - 'new SourceMapMetadataConsumer(sourceMapJSON)', - () => new SourceMapMetadataConsumer(sourceMapJSON), - ); - hookParsedMetadata.sourceMapConsumer = withSyncPerfMeasurements( - 'new SourceMapCodecConsumer(sourceMapJSON)', - () => SourceMapCodecConsumer(sourceMapJSON), - ); + const {runtimeSourceURL, sourceMapJSON} = hookSourceAndMetadata; - const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; + // If we've already loaded the source map info for this file, + // we can skip reloading it (and more importantly, re-parsing it). + const runtimeMetadata = runtimeURLToMetadataCache.get(runtimeSourceURL); + if (runtimeMetadata != null) { + if (__DEBUG__) { + console.groupCollapsed( + `parseHookNames() Found cached runtime metadata for file "${runtimeSourceURL}"`, + ); + console.log(runtimeMetadata); + console.groupEnd(); + } - // Only set once to avoid triggering eviction/cleanup code. - if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) { - if (__DEBUG__) { - console.log( - `parseSourceMaps() Caching runtime metadata for "${runtimeSourceURL}"`, - ); - } + hookParsedMetadata.metadataConsumer = runtimeMetadata.metadataConsumer; + hookParsedMetadata.sourceConsumer = runtimeMetadata.sourceConsumer; + hookParsedMetadata.sourceMapConsumer = runtimeMetadata.sourceMapConsumer; + } else { + if (sourceMapJSON != null) { + const sourceMapConsumer = withSyncPerfMeasurements( + 'new SourceMapCodecConsumer(sourceMapJSON)', + () => SourceMapCodecConsumer(sourceMapJSON), + ); + + const metadataConsumer = withSyncPerfMeasurements( + 'new SourceMapMetadataConsumer(sourceMapJSON)', + () => new SourceMapMetadataConsumer(sourceMapJSON), + ); + + hookParsedMetadata.metadataConsumer = metadataConsumer; + hookParsedMetadata.sourceMapConsumer = sourceMapConsumer; + // Only set once to avoid triggering eviction/cleanup code. runtimeURLToMetadataCache.set(runtimeSourceURL, { - metadataConsumer: hookParsedMetadata.metadataConsumer, + metadataConsumer: metadataConsumer, sourceConsumer: null, - sourceMapConsumer: hookParsedMetadata.sourceMapConsumer, + sourceMapConsumer: sourceMapConsumer, }); } } From ffd47841c906c95c675b150e18d64f26fa3230a7 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 22 Sep 2021 19:11:24 -0400 Subject: [PATCH 3/3] Deleted old source-map-js integration branch in parseSourceAndMetadata --- .../src/hooks/astUtils.js | 2 - .../parseHookNames/parseSourceAndMetadata.js | 318 +----------------- 2 files changed, 18 insertions(+), 302 deletions(-) diff --git a/packages/react-devtools-shared/src/hooks/astUtils.js b/packages/react-devtools-shared/src/hooks/astUtils.js index 9a2405920568c..2fddf027e0ffc 100644 --- a/packages/react-devtools-shared/src/hooks/astUtils.js +++ b/packages/react-devtools-shared/src/hooks/astUtils.js @@ -18,8 +18,6 @@ export type Position = {| column: number, |}; -export type SourceConsumer = any; - export type SourceFileASTWithHookDetails = { sourceFileAST: File, line: number, diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 36eed8f589d14..7eada16a7d9e0 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -12,7 +12,6 @@ import {parse} from '@babel/parser'; 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'; @@ -22,7 +21,7 @@ import { withAsyncPerfMeasurements, withSyncPerfMeasurements, } from 'react-devtools-shared/src/PerformanceLoggingUtils'; -import SourceMapCodecConsumer from '../SourceMapConsumer'; +import SourceMapConsumer from '../SourceMapConsumer'; import type {SourceMapConsumerType} from '../SourceMapConsumer'; import type { @@ -31,9 +30,6 @@ import type { } from './loadSourceAndMetadata'; import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; import type {HookNames, LRUCache} from 'react-devtools-shared/src/types'; -import type {SourceConsumer} from '../astUtils'; - -const USE_ALTERNATE_SOURCE_MAP = true; type AST = mixed; @@ -57,9 +53,6 @@ type HookParsedMetadata = {| // Column number in original source code. originalSourceColumnNumber: number | null, - // APIs from source-map for parsing source maps (if detected). - sourceConsumer: SourceConsumer | null, - // Alternate APIs from source-map for parsing source maps (if detected). sourceMapConsumer: SourceMapConsumerType | null, |}; @@ -68,29 +61,13 @@ type LocationKeyToHookParsedMetadata = Map; type CachedRuntimeCodeMetadata = {| metadataConsumer: SourceMapMetadataConsumer | null, - sourceConsumer: SourceConsumer | null, sourceMapConsumer: SourceMapConsumerType | null, |}; const runtimeURLToMetadataCache: LRUCache< string, CachedRuntimeCodeMetadata, -> = new LRU({ - max: 50, - dispose: (runtimeSourceURL: string, metadata: CachedRuntimeCodeMetadata) => { - if (__DEBUG__) { - console.log( - `runtimeURLToMetadataCache.dispose() Evicting cached metadata for "${runtimeSourceURL}"`, - ); - } - -console.log(`runtimeURLToMetadataCache() dispose of "${runtimeSourceURL}"`); - const sourceConsumer = metadata.sourceConsumer; - if (sourceConsumer !== null) { - sourceConsumer.destroy(); - } - }, -}); +> = new LRU({max: 50}); type CachedSourceCodeMetadata = {| originalSourceAST: AST, @@ -121,35 +98,19 @@ export async function parseSourceAndMetadata( () => initializeHookParsedMetadata(locationKeyToHookSourceAndMetadata), ); - if (USE_ALTERNATE_SOURCE_MAP) { - withSyncPerfMeasurements('parseSourceMapsAlternate', () => - parseSourceMapsAlternate( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); - - withSyncPerfMeasurements('parseSourceASTAlternate()', () => - parseSourceASTAlternate( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); - } else { - withSyncPerfMeasurements('parseSourceMaps', () => - parseSourceMaps( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); + withSyncPerfMeasurements('parseSourceMaps', () => + parseSourceMaps( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); - withSyncPerfMeasurements('parseSourceAST()', () => - parseSourceAST( - locationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata, - ), - ); - } + withSyncPerfMeasurements('parseSourceAST()', () => + parseSourceAST( + locationKeyToHookSourceAndMetadata, + locationKeyToHookParsedMetadata, + ), + ); return withSyncPerfMeasurements('findHookNames()', () => findHookNames(hooksList, locationKeyToHookParsedMetadata), @@ -245,7 +206,6 @@ function initializeHookParsedMetadata( originalSourceURL: null, originalSourceLineNumber: null, originalSourceColumnNumber: null, - sourceConsumer: null, sourceMapConsumer: null, }; @@ -289,194 +249,6 @@ function parseSourceAST( throw Error('Hook source code location not found.'); } - const {metadataConsumer, sourceConsumer} = hookParsedMetadata; - const runtimeSourceCode = ((hookSourceAndMetadata.runtimeSourceCode: any): string); - - let hasHookMap = false; - let originalSourceURL; - let originalSourceCode; - let originalSourceColumnNumber; - let originalSourceLineNumber; - if (areSourceMapsAppliedToErrors() || sourceConsumer == null) { - // Either the current environment automatically applies source maps to errors, - // or the current code had no source map to begin with. - // Either way, we don't need to convert the Error stack frame locations. - originalSourceColumnNumber = columnNumber; - originalSourceLineNumber = lineNumber; - // There's no source map to parse here so we can just parse the original source itself. - originalSourceCode = runtimeSourceCode; - // TODO (named hooks) This mixes runtimeSourceURLs with source mapped URLs in the same cache key space. - // Namespace them? - originalSourceURL = hookSourceAndMetadata.runtimeSourceURL; - } else { - // Parse and extract the AST from the source map. - // Now that the source map has been loaded, - // extract the original source for later. - // TODO (named hooks) Refactor this read, github.com/facebook/react/pull/22181 - const {column, line, source} = withSyncPerfMeasurements( - 'sourceConsumer.originalPositionFor()', - () => - sourceConsumer.originalPositionFor({ - line: lineNumber, - - // Column numbers are represented differently between tools/engines. - // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based. - // For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991 - column: columnNumber - 1, - }), - ); - - if (source == null) { - // TODO (named hooks) maybe fall back to the runtime source instead of throwing? - throw new Error( - 'Could not map hook runtime location to original source location', - ); - } - - originalSourceColumnNumber = column; - originalSourceLineNumber = line; - // TODO (named hooks) maybe canonicalize this URL somehow? - // 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); - originalSourceCode = withSyncPerfMeasurements( - 'sourceConsumer.sourceContentFor()', - () => (sourceConsumer.sourceContentFor(source, true): string), - ); - - if (__DEBUG__) { - console.groupCollapsed( - `parseSourceAST() Extracted source code from source map for "${originalSourceURL}"`, - ); - console.log(originalSourceCode); - console.groupEnd(); - } - - if ( - metadataConsumer != null && - metadataConsumer.hasHookMap(originalSourceURL) - ) { - hasHookMap = true; - } - } - - if (__DEBUG__) { - console.log( - `parseSourceAST() mapped line ${lineNumber}->${originalSourceLineNumber} and column ${columnNumber}->${originalSourceColumnNumber}`, - ); - } - - hookParsedMetadata.originalSourceCode = originalSourceCode; - hookParsedMetadata.originalSourceURL = originalSourceURL; - hookParsedMetadata.originalSourceLineNumber = originalSourceLineNumber; - hookParsedMetadata.originalSourceColumnNumber = originalSourceColumnNumber; - - if (hasHookMap) { - if (__DEBUG__) { - console.log( - `parseSourceAST() Found hookMap and skipping parsing for "${originalSourceURL}"`, - ); - } - // If there's a hook map present from an extended sourcemap then - // we don't need to parse the source files and instead can use the - // hook map to extract hook names. - return; - } - - if (__DEBUG__) { - console.log( - `parseSourceAST() Did not find hook map for "${originalSourceURL}"`, - ); - } - - // The cache also serves to deduplicate parsing by URL in our loop over location keys. - // This may need to change if we switch to async parsing. - const sourceMetadata = originalURLToMetadataCache.get(originalSourceURL); - if (sourceMetadata != null) { - if (__DEBUG__) { - console.groupCollapsed( - `parseSourceAST() Found cached source metadata for "${originalSourceURL}"`, - ); - console.log(sourceMetadata); - console.groupEnd(); - } - hookParsedMetadata.originalSourceAST = sourceMetadata.originalSourceAST; - hookParsedMetadata.originalSourceCode = - sourceMetadata.originalSourceCode; - } else { - try { - // TypeScript is the most commonly used typed JS variant so let's default to it - // unless we detect explicit Flow usage via the "@flow" pragma. - const plugin = - originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript'; - - // TODO (named hooks) This is probably where we should check max source length, - // rather than in loadSourceAndMetatada -> loadSourceFiles(). - // TODO(#22319): Support source files that are html files with inline script tags. - const originalSourceAST = withSyncPerfMeasurements( - '[@babel/parser] parse(originalSourceCode)', - () => - parse(originalSourceCode, { - sourceType: 'unambiguous', - plugins: ['jsx', plugin], - }), - ); - hookParsedMetadata.originalSourceAST = originalSourceAST; - - if (__DEBUG__) { - console.log( - `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, - ); - } - - originalURLToMetadataCache.set(originalSourceURL, { - originalSourceAST, - originalSourceCode, - }); - } catch (error) { - throw new Error( - `Failed to parse source file: ${originalSourceURL}\n\n` + - `Original error: ${error}`, - ); - } - } - }, - ); -} - -function parseSourceASTAlternate( - locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata, -): void { - locationKeyToHookSourceAndMetadata.forEach( - (hookSourceAndMetadata, locationKey) => { - const hookParsedMetadata = locationKeyToHookParsedMetadata.get( - locationKey, - ); - if (hookParsedMetadata == null) { - throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); - } - - if (hookParsedMetadata.originalSourceAST !== null) { - // Use cached metadata. - return; - } - - if ( - hookParsedMetadata.originalSourceURL != null && - hookParsedMetadata.originalSourceCode != null && - hookParsedMetadata.originalSourceColumnNumber != null && - hookParsedMetadata.originalSourceLineNumber != null - ) { - // Use cached metadata. - return; - } - - const {lineNumber, columnNumber} = hookSourceAndMetadata.hookSource; - if (lineNumber == null || columnNumber == null) { - throw Error('Hook source code location not found.'); - } - const {metadataConsumer, sourceMapConsumer} = hookParsedMetadata; const runtimeSourceCode = ((hookSourceAndMetadata.runtimeSourceCode: any): string); let hasHookMap = false; @@ -616,59 +388,6 @@ function parseSourceMaps( throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); } - const sourceMapJSON = hookSourceAndMetadata.sourceMapJSON; - - if (hookParsedMetadata.sourceConsumer === null) { - if (sourceMapJSON != null) { - hookParsedMetadata.sourceConsumer = withSyncPerfMeasurements( - 'new SourceMapConsumer(sourceMapJSON)', - () => new SourceMapConsumer(sourceMapJSON), - ); - } - } - - if (hookParsedMetadata.metadataConsumer === null) { - if (sourceMapJSON != null) { - hookParsedMetadata.metadataConsumer = withSyncPerfMeasurements( - 'new SourceMapMetadataConsumer(sourceMapJSON)', - () => new SourceMapMetadataConsumer(sourceMapJSON), - ); - } - } - - const runtimeSourceURL = hookSourceAndMetadata.runtimeSourceURL; - - // Only set once to avoid triggering eviction/cleanup code. - if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) { - if (__DEBUG__) { - console.log( - `parseSourceMaps() Caching runtime metadata for "${runtimeSourceURL}"`, - ); - } - - runtimeURLToMetadataCache.set(runtimeSourceURL, { - metadataConsumer: hookParsedMetadata.metadataConsumer, - sourceConsumer: hookParsedMetadata.sourceConsumer, - sourceMapConsumer: null, - }); - } - }, - ); -} - -function parseSourceMapsAlternate( - locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata, - locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata, -) { - locationKeyToHookSourceAndMetadata.forEach( - (hookSourceAndMetadata, locationKey) => { - const hookParsedMetadata = locationKeyToHookParsedMetadata.get( - locationKey, - ); - if (hookParsedMetadata == null) { - throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`); - } - const {runtimeSourceURL, sourceMapJSON} = hookSourceAndMetadata; // If we've already loaded the source map info for this file, @@ -684,13 +403,13 @@ function parseSourceMapsAlternate( } hookParsedMetadata.metadataConsumer = runtimeMetadata.metadataConsumer; - hookParsedMetadata.sourceConsumer = runtimeMetadata.sourceConsumer; - hookParsedMetadata.sourceMapConsumer = runtimeMetadata.sourceMapConsumer; + hookParsedMetadata.sourceMapConsumer = + runtimeMetadata.sourceMapConsumer; } else { if (sourceMapJSON != null) { const sourceMapConsumer = withSyncPerfMeasurements( - 'new SourceMapCodecConsumer(sourceMapJSON)', - () => SourceMapCodecConsumer(sourceMapJSON), + 'new SourceMapConsumer(sourceMapJSON)', + () => SourceMapConsumer(sourceMapJSON), ); const metadataConsumer = withSyncPerfMeasurements( @@ -704,7 +423,6 @@ function parseSourceMapsAlternate( // Only set once to avoid triggering eviction/cleanup code. runtimeURLToMetadataCache.set(runtimeSourceURL, { metadataConsumer: metadataConsumer, - sourceConsumer: null, sourceMapConsumer: sourceMapConsumer, }); }