-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DevTools: Hook names optimizations (#22403)
This commit dramatically improves the performance of the hook names feature by replacing the source-map-js integration with custom mapping code built on top of sourcemap-codec. Based on my own benchmarking, this makes parsing 3-4 times faster. (The bulk of these changes are in SourceMapConsumer.js.) While implementing this code, I also uncovered a problem with the way we were caching source-map metadata that was causing us to potential parse the same source-map multiple times. (I addressed this in a separate commit for easier reviewing. The bulk of these changes are in parseSourceAndMetadata.js.) Altogether these changes dramatically improve the performance of the hooks parsing code. One additional thing we could look into if the source-map download still remains a large bottleneck would be to stream it and decode the mappings array while it streams in rather than in one synchronous chunk after the full source-map has been downloaded.
- Loading branch information
Brian Vaughn
authored
Sep 23, 2021
1 parent
95502f7
commit d174d06
Showing
3 changed files
with
303 additions
and
111 deletions.
There are no files selected for viewing
241 changes: 241 additions & 0 deletions
241
packages/react-devtools-shared/src/hooks/SourceMapConsumer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Array<Array<number>>>; | ||
|
||
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.