diff --git a/benchmark/module/module-require-source-map.js b/benchmark/module/module-require-source-map.js new file mode 100644 index 000000000000..8353ab39fc82 --- /dev/null +++ b/benchmark/module/module-require-source-map.js @@ -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); +} diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js index 95b09090c473..e1fa55c5caba 100644 --- a/lib/internal/source_map/source_map_cache.js +++ b/lib/internal/source_map/source_map_cache.js @@ -9,6 +9,7 @@ const { RegExpPrototypeSymbolSplit, SafeMap, StringPrototypeCodePointAt, + StringPrototypeIndexOf, StringPrototypeSplit, StringPrototypeStartsWith, } = primordials; @@ -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, }; @@ -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 @@ -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. * @@ -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; }