Skip to content

Commit c4bd0b8

Browse files
committed
lib: defer source map payload decoding until first use
With source maps enabled, every module that carries a sourceMappingURL had its map decoded (or read from disk), JSON-parsed and its sources resolved to absolute URLs while the module was being loaded, and the per-line length table used for coverage was built with a per-code-point loop. None of that is needed unless a stack trace is later mapped. Keep the URL on the cache entry and resolve the payload on the first findSourceMap() for that file. Under NODE_V8_COVERAGE the payload is still resolved at load time, since the cache is serialized during shutdown. lineLengths() now splits on '\n' with indexOf and only falls back to the code point walk when the source contains U+2028/U+2029. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 21f0f27 commit c4bd0b8

2 files changed

Lines changed: 122 additions & 15 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
// Loading modules that carry source maps, with source map support enabled.
4+
// This is the cost paid at startup by applications bundled or transpiled with
5+
// source maps; the maps are only consulted if a stack trace is generated.
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
const common = require('../common.js');
10+
const tmpdir = require('../../test/common/tmpdir');
11+
const benchmarkDirectory = tmpdir.resolve('nodejs-benchmark-module-source-map');
12+
13+
const bench = common.createBenchmark(main, {
14+
sourceMap: ['none', 'inline', 'external'],
15+
n: [1000],
16+
}, {
17+
setup(configs) {
18+
tmpdir.refresh();
19+
const maxN = configs.reduce((max, c) => Math.max(max, c.n), 0);
20+
createModules(maxN);
21+
},
22+
});
23+
24+
function moduleSource(i) {
25+
const methods = [];
26+
for (let m = 0; m < 40; m++) {
27+
methods.push(` method${m}(input) { return [].concat(input).map((item) => ({ item, m: ${m}, service: ${i} })); }`);
28+
}
29+
return `'use strict';
30+
class Service${i} {
31+
constructor(options = {}) { this.options = { retries: 3, ...options }; }
32+
${methods.join('\n')}
33+
}
34+
function helper${i}(list) { return list.filter(Boolean).slice(0, ${i % 7}); }
35+
module.exports = { Service${i}, helper${i} };
36+
`;
37+
}
38+
39+
function sourceMapFor(i, source) {
40+
return JSON.stringify({
41+
version: 3,
42+
file: `${i}.js`,
43+
sources: [`../src/${i}.ts`],
44+
sourcesContent: [source],
45+
names: [],
46+
mappings: 'AAAA;' + 'AACA,MAAM;'.repeat(44),
47+
});
48+
}
49+
50+
function createModules(n) {
51+
for (const kind of ['none', 'inline', 'external']) {
52+
const dir = path.join(benchmarkDirectory, kind);
53+
fs.mkdirSync(dir, { recursive: true });
54+
for (let i = 0; i < n; i++) {
55+
const source = moduleSource(i);
56+
let trailer = '';
57+
if (kind === 'inline') {
58+
const data = Buffer.from(sourceMapFor(i, source)).toString('base64');
59+
trailer = `//# sourceMappingURL=data:application/json;base64,${data}\n`;
60+
} else if (kind === 'external') {
61+
fs.writeFileSync(path.join(dir, `${i}.js.map`), sourceMapFor(i, source));
62+
trailer = `//# sourceMappingURL=${i}.js.map\n`;
63+
}
64+
fs.writeFileSync(path.join(dir, `${i}.js`), source + trailer);
65+
}
66+
}
67+
}
68+
69+
function main({ sourceMap, n }) {
70+
process.setSourceMapsEnabled(true);
71+
const dir = path.join(benchmarkDirectory, sourceMap);
72+
bench.start();
73+
for (let i = 0; i < n; i++) {
74+
require(path.join(dir, `${i}.js`));
75+
}
76+
bench.end(n);
77+
}

lib/internal/source_map/source_map_cache.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
RegExpPrototypeSymbolSplit,
1010
SafeMap,
1111
StringPrototypeCodePointAt,
12+
StringPrototypeIndexOf,
1213
StringPrototypeSplit,
1314
StringPrototypeStartsWith,
1415
} = primordials;
@@ -176,16 +177,14 @@ function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSourc
176177
// Normalize the sourceURL to a file URL if it is a path.
177178
sourceURL = normalizeReferrerURL(sourceURL);
178179

179-
const data = dataFromUrl(filename, sourceMapURL);
180-
// `data` could be null if the source map is invalid.
181-
// In this case, create a cache entry with null data with source url for test coverage.
182-
180+
// The payload is resolved on first use (see sourceMapData()), except under
181+
// coverage, where it is serialized at exit when no more JS may run.
183182
const entry = {
184183
__proto__: null,
185184
lineLengths: lineLengths(content),
186-
data,
187-
// Save the source map url if it is not a data url.
188-
sourceMapURL: data ? null : sourceMapURL,
185+
data: process.env.NODE_V8_COVERAGE ? dataFromUrl(filename, sourceMapURL) : undefined,
186+
filename,
187+
sourceMapURL,
189188
sourceURL,
190189
};
191190

@@ -254,20 +253,36 @@ function dataFromUrl(sourceURL, sourceMappingURL) {
254253
return sourceMapFromFile(mapURL);
255254
}
256255

256+
const kUnicodeLineTerminators = /[\u2028\u2029]/;
257+
257258
// Cache the length of each line in the file that a source map was extracted
258259
// from. This allows translation from byte offset V8 coverage reports,
259260
// to line/column offset Source Map V3.
260261
function lineLengths(content) {
262+
if (RegExpPrototypeExec(kUnicodeLineTerminators, content) !== null) {
263+
return lineLengthsWithUnicodeTerminators(content);
264+
}
265+
// We purposefully keep \r as part of the line-length calculation, in
266+
// cases where there is a \r\n separator, so that this can be taken into
267+
// account in coverage calculations.
268+
const output = [];
269+
let lineStart = 0;
270+
let lineEnd;
271+
while ((lineEnd = StringPrototypeIndexOf(content, '\n', lineStart)) !== -1) {
272+
ArrayPrototypePush(output, lineEnd - lineStart);
273+
lineStart = lineEnd + 1;
274+
}
275+
ArrayPrototypePush(output, content.length - lineStart);
276+
return output;
277+
}
278+
279+
function lineLengthsWithUnicodeTerminators(content) {
261280
const contentLength = content.length;
262281
const output = [];
263282
let lineLength = 0;
264283
for (let i = 0; i < contentLength; i++, lineLength++) {
265284
const codePoint = StringPrototypeCodePointAt(content, i);
266-
267-
// We purposefully keep \r as part of the line-length calculation, in
268-
// cases where there is a \r\n separator, so that this can be taken into
269-
// account in coverage calculations.
270-
// codepoints for \n (new line), \u2028 (line separator) and \u2029 (paragraph separator)
285+
// \n (new line), \u2028 (line separator) and \u2029 (paragraph separator)
271286
if (codePoint === 10 || codePoint === 0x2028 || codePoint === 0x2029) {
272287
ArrayPrototypePush(output, lineLength);
273288
lineLength = -1; // To not count the matched codePoint such as \n character
@@ -351,16 +366,31 @@ function sourceMapCacheToObject() {
351366

352367
const obj = { __proto__: null };
353368
for (const { 0: k, 1: v } of moduleSourceMapCache) {
369+
const data = v.data ?? null;
354370
obj[k] = {
355371
__proto__: null,
356372
lineLengths: v.lineLengths,
357-
data: v.data,
358-
url: v.sourceMapURL,
373+
data,
374+
// Save the source map url if it is not a data url.
375+
url: data ? null : v.sourceMapURL,
359376
};
360377
}
361378
return obj;
362379
}
363380

381+
/**
382+
* Resolve and parse the payload of a cache entry the first time it is needed;
383+
* `null` marks a source map that could not be loaded.
384+
* @param {object} entry
385+
* @returns {object|null}
386+
*/
387+
function sourceMapData(entry) {
388+
if (entry.data === undefined) {
389+
entry.data = dataFromUrl(entry.filename, entry.sourceMapURL);
390+
}
391+
return entry.data;
392+
}
393+
364394
/**
365395
* Find a source map for a given actual source URL or path.
366396
*
@@ -390,7 +420,7 @@ function findSourceMap(sourceURL) {
390420
sourceURL = pathToFileURL(sourceURL).href;
391421
}
392422
const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
393-
if (entry?.data == null) {
423+
if (entry === undefined || sourceMapData(entry) === null) {
394424
return undefined;
395425
}
396426

0 commit comments

Comments
 (0)