Skip to content

Commit d174d06

Browse files
author
Brian Vaughn
authored
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.
1 parent 95502f7 commit d174d06

File tree

3 files changed

+303
-111
lines changed

3 files changed

+303
-111
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
10+
import {decode} from 'sourcemap-codec';
11+
12+
import type {
13+
IndexSourceMap,
14+
BasicSourceMap,
15+
MixedSourceMap,
16+
} from './SourceMapTypes';
17+
18+
type SearchPosition = {|
19+
columnNumber: number,
20+
lineNumber: number,
21+
|};
22+
23+
type ResultPosition = {|
24+
column: number,
25+
line: number,
26+
sourceContent: string,
27+
sourceURL: string,
28+
|};
29+
30+
export type SourceMapConsumerType = {|
31+
originalPositionFor: SearchPosition => ResultPosition,
32+
|};
33+
34+
type Mappings = Array<Array<Array<number>>>;
35+
36+
export default function SourceMapConsumer(
37+
sourceMapJSON: MixedSourceMap,
38+
): SourceMapConsumerType {
39+
if (sourceMapJSON.sections != null) {
40+
return IndexedSourceMapConsumer(((sourceMapJSON: any): IndexSourceMap));
41+
} else {
42+
return BasicSourceMapConsumer(((sourceMapJSON: any): BasicSourceMap));
43+
}
44+
}
45+
46+
function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
47+
const decodedMappings: Mappings = withSyncPerfMeasurements(
48+
'Decoding source map mappings with sourcemap-codec',
49+
() => decode(sourceMapJSON.mappings),
50+
);
51+
52+
function originalPositionFor({
53+
columnNumber,
54+
lineNumber,
55+
}: SearchPosition): ResultPosition {
56+
// Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
57+
const targetColumnNumber = columnNumber - 1;
58+
59+
const lineMappings = decodedMappings[lineNumber - 1];
60+
61+
let nearestEntry = null;
62+
63+
let startIndex = 0;
64+
let stopIndex = lineMappings.length - 1;
65+
let index = -1;
66+
while (startIndex <= stopIndex) {
67+
index = Math.floor((stopIndex + startIndex) / 2);
68+
nearestEntry = lineMappings[index];
69+
70+
const currentColumn = nearestEntry[0];
71+
if (currentColumn === targetColumnNumber) {
72+
break;
73+
} else {
74+
if (currentColumn > targetColumnNumber) {
75+
if (stopIndex - index > 0) {
76+
stopIndex = index;
77+
} else {
78+
index = stopIndex;
79+
break;
80+
}
81+
} else {
82+
if (index - startIndex > 0) {
83+
startIndex = index;
84+
} else {
85+
index = startIndex;
86+
break;
87+
}
88+
}
89+
}
90+
}
91+
92+
// We have found either the exact element, or the next-closest element.
93+
// However there may be more than one such element.
94+
// Make sure we always return the smallest of these.
95+
while (index > 0) {
96+
const previousEntry = lineMappings[index - 1];
97+
const currentColumn = previousEntry[0];
98+
if (currentColumn !== targetColumnNumber) {
99+
break;
100+
}
101+
index--;
102+
}
103+
104+
if (nearestEntry == null) {
105+
// TODO maybe fall back to the runtime source instead of throwing?
106+
throw Error(
107+
`Could not find runtime location for line:${lineNumber} and column:${columnNumber}`,
108+
);
109+
}
110+
111+
const sourceIndex = nearestEntry[1];
112+
const sourceContent =
113+
sourceMapJSON.sourcesContent != null
114+
? sourceMapJSON.sourcesContent[sourceIndex]
115+
: null;
116+
const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
117+
const line = nearestEntry[2] + 1;
118+
const column = nearestEntry[3];
119+
120+
if (sourceContent === null || sourceURL === null) {
121+
// TODO maybe fall back to the runtime source instead of throwing?
122+
throw Error(
123+
`Could not find original source for line:${lineNumber} and column:${columnNumber}`,
124+
);
125+
}
126+
127+
return {
128+
column,
129+
line,
130+
sourceContent: ((sourceContent: any): string),
131+
sourceURL: ((sourceURL: any): string),
132+
};
133+
}
134+
135+
return (({
136+
originalPositionFor,
137+
}: any): SourceMapConsumerType);
138+
}
139+
140+
function IndexedSourceMapConsumer(sourceMapJSON: IndexSourceMap) {
141+
let lastOffset = {
142+
line: -1,
143+
column: 0,
144+
};
145+
146+
const sections = sourceMapJSON.sections.map(section => {
147+
const offset = section.offset;
148+
const offsetLine = offset.line;
149+
const offsetColumn = offset.column;
150+
151+
if (
152+
offsetLine < lastOffset.line ||
153+
(offsetLine === lastOffset.line && offsetColumn < lastOffset.column)
154+
) {
155+
throw new Error('Section offsets must be ordered and non-overlapping.');
156+
}
157+
158+
lastOffset = offset;
159+
160+
return {
161+
// The offset fields are 0-based, but we use 1-based indices when encoding/decoding from VLQ.
162+
generatedLine: offsetLine + 1,
163+
generatedColumn: offsetColumn + 1,
164+
sourceMapConsumer: new SourceMapConsumer(section.map),
165+
};
166+
});
167+
168+
function originalPositionFor({
169+
columnNumber,
170+
lineNumber,
171+
}: SearchPosition): ResultPosition {
172+
// Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
173+
const targetColumnNumber = columnNumber - 1;
174+
175+
let section = null;
176+
177+
let startIndex = 0;
178+
let stopIndex = sections.length - 1;
179+
let index = -1;
180+
while (startIndex <= stopIndex) {
181+
index = Math.floor((stopIndex + startIndex) / 2);
182+
section = sections[index];
183+
184+
const currentLine = section.generatedLine;
185+
if (currentLine === lineNumber) {
186+
const currentColumn = section.generatedColumn;
187+
if (currentColumn === lineNumber) {
188+
break;
189+
} else {
190+
if (currentColumn > targetColumnNumber) {
191+
if (stopIndex - index > 0) {
192+
stopIndex = index;
193+
} else {
194+
index = stopIndex;
195+
break;
196+
}
197+
} else {
198+
if (index - startIndex > 0) {
199+
startIndex = index;
200+
} else {
201+
index = startIndex;
202+
break;
203+
}
204+
}
205+
}
206+
} else {
207+
if (currentLine > lineNumber) {
208+
if (stopIndex - index > 0) {
209+
stopIndex = index;
210+
} else {
211+
index = stopIndex;
212+
break;
213+
}
214+
} else {
215+
if (index - startIndex > 0) {
216+
startIndex = index;
217+
} else {
218+
index = startIndex;
219+
break;
220+
}
221+
}
222+
}
223+
}
224+
225+
if (section == null) {
226+
// TODO maybe fall back to the runtime source instead of throwing?
227+
throw Error(
228+
`Could not find matching section for line:${lineNumber} and column:${columnNumber}`,
229+
);
230+
}
231+
232+
return section.sourceMapConsumer.originalPositionFor({
233+
columnNumber,
234+
lineNumber,
235+
});
236+
}
237+
238+
return (({
239+
originalPositionFor,
240+
}: any): SourceMapConsumerType);
241+
}

packages/react-devtools-shared/src/hooks/astUtils.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export type Position = {|
1818
column: number,
1919
|};
2020

21-
export type SourceConsumer = any;
22-
2321
export type SourceFileASTWithHookDetails = {
2422
sourceFileAST: File,
2523
line: number,

0 commit comments

Comments
 (0)