Skip to content

Commit 37ed6dd

Browse files
committed
[sourcemaps] Improve ignore-listing performance
This improves performance of finding the relevant section in Index Source Maps by using binary search (O(N) -> O(log N)). This allows us to improve the performance of the fast path for checking if a source is ignore-listed in Index Source Maps: Instead of checking if every section in Index Source Maps ignores everything, we just find the relevant section (now O(log N) instead of O(N)) and check if that section ignores everything.
1 parent 3a41715 commit 37ed6dd

File tree

3 files changed

+76
-49
lines changed

3 files changed

+76
-49
lines changed

packages/next/src/server/dev/middleware-webpack.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { SourceMapConsumer } from 'next/dist/compiled/source-map08'
55
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
66
import { getSourceMapFromFile } from './get-source-map-from-file'
77
import {
8+
findApplicableSourceMapPayload,
89
sourceMapIgnoreListsEverything,
10+
type BasicSourceMapPayload,
911
type ModernSourceMapPayload,
1012
} from '../lib/source-maps'
1113
import { openFileInEditor } from '../../next-devtools/server/launch-editor'
@@ -50,13 +52,13 @@ type SourceAttributes = {
5052
type Source =
5153
| {
5254
type: 'file'
53-
sourceMap: ModernSourceMapPayload
55+
sourceMap: BasicSourceMapPayload
5456
ignoredSources: IgnoredSources
5557
moduleURL: string
5658
}
5759
| {
5860
type: 'bundle'
59-
sourceMap: ModernSourceMapPayload
61+
sourceMap: BasicSourceMapPayload
6062
ignoredSources: IgnoredSources
6163
compilation: webpack.Compilation
6264
moduleId: string
@@ -284,11 +286,16 @@ async function getSourceMapFromCompilation(
284286
}
285287

286288
async function getSource(
287-
sourceURL: string,
289+
frame: {
290+
file: string | null
291+
lineNumber: number | null
292+
column: number | null
293+
},
288294
options: {
289295
getCompilations: () => webpack.Compilation[]
290296
}
291297
): Promise<Source | undefined> {
298+
let sourceURL = frame.file ?? ''
292299
const { getCompilations } = options
293300

294301
// Rspack is now using file:// URLs for source maps. Remove the rsc prefix to produce the file:/// url.
@@ -308,7 +315,11 @@ async function getSource(
308315
const sourceMapPayload = nativeSourceMap.payload
309316
return {
310317
type: 'file',
311-
sourceMap: sourceMapPayload,
318+
sourceMap: findApplicableSourceMapPayload(
319+
frame.lineNumber ?? 0,
320+
frame.column ?? 0,
321+
sourceMapPayload
322+
)!,
312323

313324
ignoredSources: getIgnoredSources(
314325
// @ts-expect-error -- TODO: Support IndexSourceMap
@@ -435,7 +446,7 @@ async function getOriginalStackFrame({
435446
rootDirectory: string
436447
}): Promise<OriginalStackFrameResponse> {
437448
const filename = frame.file ?? ''
438-
const source = await getSource(filename, {
449+
const source = await getSource(frame, {
439450
getCompilations: () => {
440451
const compilations: webpack.Compilation[] = []
441452

@@ -658,23 +669,31 @@ export function getSourceMapMiddleware(options: {
658669
let source: Source | undefined
659670

660671
try {
661-
source = await getSource(filename, {
662-
getCompilations: () => {
663-
const compilations: webpack.Compilation[] = []
664-
665-
for (const stats of [
666-
clientStats(),
667-
serverStats(),
668-
edgeServerStats(),
669-
]) {
670-
if (stats?.compilation) {
671-
compilations.push(stats.compilation)
672+
source = await getSource(
673+
{
674+
file: filename,
675+
// Webpack doesn't use Index Source Maps
676+
lineNumber: null,
677+
column: null,
678+
},
679+
{
680+
getCompilations: () => {
681+
const compilations: webpack.Compilation[] = []
682+
683+
for (const stats of [
684+
clientStats(),
685+
serverStats(),
686+
edgeServerStats(),
687+
]) {
688+
if (stats?.compilation) {
689+
compilations.push(stats.compilation)
690+
}
672691
}
673-
}
674692

675-
return compilations
676-
},
677-
})
693+
return compilations
694+
},
695+
}
696+
)
678697
} catch (error) {
679698
return middlewareResponse.internalServerError(res, error)
680699
}

packages/next/src/server/lib/source-maps.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface IndexSourceMap {
1818
}
1919

2020
/** https://tc39.es/ecma426/#sec-source-map-format */
21-
interface BasicSourceMapPayload {
21+
export interface BasicSourceMapPayload {
2222
version: number
2323
// TODO: Move to https://github.com/jridgewell/sourcemaps which is actively maintained
2424
/** WARNING: `file` is optional. */
@@ -34,17 +34,9 @@ interface BasicSourceMapPayload {
3434

3535
export type ModernSourceMapPayload = BasicSourceMapPayload | IndexSourceMap
3636

37-
// TODO: This should take the BasicSourceMapPayload. Only the relevant section
38-
// needs to ignore list everything making this check effectively O(1) multiplied
39-
// by the complexity of finding the section.
4037
export function sourceMapIgnoreListsEverything(
41-
sourceMap: ModernSourceMapPayload
38+
sourceMap: BasicSourceMapPayload
4239
): boolean {
43-
if ('sections' in sourceMap) {
44-
return sourceMap.sections.every((section) => {
45-
return sourceMapIgnoreListsEverything(section.map)
46-
})
47-
}
4840
return (
4941
sourceMap.ignoreList !== undefined &&
5042
sourceMap.sources.length === sourceMap.ignoreList.length
@@ -56,26 +48,40 @@ export function sourceMapIgnoreListsEverything(
5648
* Equal to the input unless an Index Source Map is used.
5749
*/
5850
export function findApplicableSourceMapPayload(
59-
lineNumber: number,
60-
columnNumber: number,
51+
line: number,
52+
column: number,
6153
payload: ModernSourceMapPayload
6254
): BasicSourceMapPayload | undefined {
6355
if ('sections' in payload) {
56+
if (payload.sections.length === 0) {
57+
return undefined
58+
}
59+
6460
// Sections must not overlap and must be sorted: https://tc39.es/source-map/#section-object
6561
// Therefore the last section that has an offset less than or equal to the frame is the applicable one.
66-
// TODO(veil): Binary search
67-
let section: IndexSourceMapSection | undefined = payload.sections[0]
68-
for (
69-
let i = 0;
70-
i < payload.sections.length &&
71-
payload.sections[i].offset.line <= lineNumber &&
72-
payload.sections[i].offset.column <= columnNumber;
73-
i++
74-
) {
75-
section = payload.sections[i]
62+
const sections = payload.sections
63+
let left = 0
64+
let right = sections.length - 1
65+
let result: IndexSourceMapSection | null = null
66+
67+
while (left <= right) {
68+
// fast Math.floor
69+
const middle = ~~((left + right) / 2)
70+
const section = sections[middle]
71+
const offset = section.offset
72+
73+
if (
74+
offset.line < line ||
75+
(offset.line === line && offset.column <= column)
76+
) {
77+
result = section
78+
left = middle + 1
79+
} else {
80+
right = middle - 1
81+
}
7682
}
7783

78-
return section === undefined ? undefined : section.map
84+
return result === null ? undefined : result.map
7985
} else {
8086
return payload
8187
}

packages/next/src/server/patch-error-inspect.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,14 @@ function getSourcemappedFrameIfPossible(
208208
line: frame.lineNumber ?? 1,
209209
})
210210

211-
let ignored = sourceMapIgnoreListsEverything(sourceMapPayload)
211+
const applicableSourceMap = findApplicableSourceMapPayload(
212+
frame.lineNumber ?? 0,
213+
frame.column ?? 0,
214+
sourceMapPayload
215+
)
216+
let ignored =
217+
applicableSourceMap !== undefined &&
218+
sourceMapIgnoreListsEverything(applicableSourceMap)
212219
if (sourcePosition.source === null) {
213220
return {
214221
stack: {
@@ -223,11 +230,6 @@ function getSourcemappedFrameIfPossible(
223230
}
224231
}
225232

226-
const applicableSourceMap = findApplicableSourceMapPayload(
227-
frame.lineNumber ?? 0,
228-
frame.column ?? 0,
229-
sourceMapPayload
230-
)
231233
// TODO(veil): Upstream a method to sourcemap consumer that immediately says if a frame is ignored or not.
232234
if (applicableSourceMap === undefined) {
233235
console.error('No applicable source map found in sections for frame', frame)

0 commit comments

Comments
 (0)