Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions benchmark/module/module-require-source-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict';

// Loading modules that carry source maps, with source map support enabled.
// This is the cost paid at startup by applications bundled or transpiled with
// source maps; the maps are only consulted if a stack trace is generated.

const fs = require('fs');
const path = require('path');
const common = require('../common.js');
const tmpdir = require('../../test/common/tmpdir');
const benchmarkDirectory = tmpdir.resolve('nodejs-benchmark-module-source-map');

const bench = common.createBenchmark(main, {
sourceMap: ['none', 'inline', 'external'],
n: [1000],
}, {
setup(configs) {
tmpdir.refresh();
const maxN = configs.reduce((max, c) => Math.max(max, c.n), 0);
createModules(maxN);
},
});

function moduleSource(i) {
const methods = [];
for (let m = 0; m < 40; m++) {
methods.push(` method${m}(input) { return [].concat(input).map((item) => ({ item, m: ${m}, service: ${i} })); }`);
}
return `'use strict';
class Service${i} {
constructor(options = {}) { this.options = { retries: 3, ...options }; }
${methods.join('\n')}
}
function helper${i}(list) { return list.filter(Boolean).slice(0, ${i % 7}); }
module.exports = { Service${i}, helper${i} };
`;
}

function sourceMapFor(i, source) {
return JSON.stringify({
version: 3,
file: `${i}.js`,
sources: [`../src/${i}.ts`],
sourcesContent: [source],
names: [],
mappings: 'AAAA;' + 'AACA,MAAM;'.repeat(44),
});
}

function createModules(n) {
for (const kind of ['none', 'inline', 'external']) {
const dir = path.join(benchmarkDirectory, kind);
fs.mkdirSync(dir, { recursive: true });
for (let i = 0; i < n; i++) {
const source = moduleSource(i);
let trailer = '';
if (kind === 'inline') {
const data = Buffer.from(sourceMapFor(i, source)).toString('base64');
trailer = `//# sourceMappingURL=data:application/json;base64,${data}\n`;
} else if (kind === 'external') {
fs.writeFileSync(path.join(dir, `${i}.js.map`), sourceMapFor(i, source));
trailer = `//# sourceMappingURL=${i}.js.map\n`;
}
fs.writeFileSync(path.join(dir, `${i}.js`), source + trailer);
}
}
}

function main({ sourceMap, n }) {
process.setSourceMapsEnabled(true);
const dir = path.join(benchmarkDirectory, sourceMap);
bench.start();
for (let i = 0; i < n; i++) {
require(path.join(dir, `${i}.js`));
}
bench.end(n);
}
60 changes: 45 additions & 15 deletions lib/internal/source_map/source_map_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
RegExpPrototypeSymbolSplit,
SafeMap,
StringPrototypeCodePointAt,
StringPrototypeIndexOf,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
Expand Down Expand Up @@ -176,16 +177,14 @@ function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSourc
// Normalize the sourceURL to a file URL if it is a path.
sourceURL = normalizeReferrerURL(sourceURL);

const data = dataFromUrl(filename, sourceMapURL);
// `data` could be null if the source map is invalid.
// In this case, create a cache entry with null data with source url for test coverage.

// The payload is resolved on first use (see sourceMapData()), except under
// coverage, where it is serialized at exit when no more JS may run.
const entry = {
__proto__: null,
lineLengths: lineLengths(content),
data,
// Save the source map url if it is not a data url.
sourceMapURL: data ? null : sourceMapURL,
data: process.env.NODE_V8_COVERAGE ? dataFromUrl(filename, sourceMapURL) : undefined,
filename,
sourceMapURL,
sourceURL,
};

Expand Down Expand Up @@ -254,20 +253,36 @@ function dataFromUrl(sourceURL, sourceMappingURL) {
return sourceMapFromFile(mapURL);
}

const kUnicodeLineTerminators = /[\u2028\u2029]/;

// Cache the length of each line in the file that a source map was extracted
// from. This allows translation from byte offset V8 coverage reports,
// to line/column offset Source Map V3.
function lineLengths(content) {
if (RegExpPrototypeExec(kUnicodeLineTerminators, content) !== null) {
return lineLengthsWithUnicodeTerminators(content);
}
// We purposefully keep \r as part of the line-length calculation, in
// cases where there is a \r\n separator, so that this can be taken into
// account in coverage calculations.
const output = [];
let lineStart = 0;
let lineEnd;
while ((lineEnd = StringPrototypeIndexOf(content, '\n', lineStart)) !== -1) {
ArrayPrototypePush(output, lineEnd - lineStart);
lineStart = lineEnd + 1;
}
ArrayPrototypePush(output, content.length - lineStart);
return output;
}

function lineLengthsWithUnicodeTerminators(content) {
const contentLength = content.length;
const output = [];
let lineLength = 0;
for (let i = 0; i < contentLength; i++, lineLength++) {
const codePoint = StringPrototypeCodePointAt(content, i);

// We purposefully keep \r as part of the line-length calculation, in
// cases where there is a \r\n separator, so that this can be taken into
// account in coverage calculations.
// codepoints for \n (new line), \u2028 (line separator) and \u2029 (paragraph separator)
// \n (new line), \u2028 (line separator) and \u2029 (paragraph separator)
if (codePoint === 10 || codePoint === 0x2028 || codePoint === 0x2029) {
ArrayPrototypePush(output, lineLength);
lineLength = -1; // To not count the matched codePoint such as \n character
Expand Down Expand Up @@ -351,16 +366,31 @@ function sourceMapCacheToObject() {

const obj = { __proto__: null };
for (const { 0: k, 1: v } of moduleSourceMapCache) {
const data = v.data ?? null;
obj[k] = {
__proto__: null,
lineLengths: v.lineLengths,
data: v.data,
url: v.sourceMapURL,
data,
// Save the source map url if it is not a data url.
url: data ? null : v.sourceMapURL,
};
}
return obj;
}

/**
* Resolve and parse the payload of a cache entry the first time it is needed;
* `null` marks a source map that could not be loaded.
* @param {object} entry
* @returns {object|null}
*/
function sourceMapData(entry) {
if (entry.data === undefined) {
entry.data = dataFromUrl(entry.filename, entry.sourceMapURL);
}
return entry.data;
}

/**
* Find a source map for a given actual source URL or path.
*
Expand Down Expand Up @@ -390,7 +420,7 @@ function findSourceMap(sourceURL) {
sourceURL = pathToFileURL(sourceURL).href;
}
const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
if (entry?.data == null) {
if (entry === undefined || sourceMapData(entry) === null) {
return undefined;
}

Expand Down
Loading