Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src,lib: print source map error source on demand #43875

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -6,6 +6,7 @@ test/message/esm_display_syntax_error.mjs
tools/icu
tools/lint-md/lint-md.mjs
benchmark/tmp
benchmark/fixtures
doc/**/*.js
!doc/api_assets/*.js
!.eslintrc.js
34 changes: 34 additions & 0 deletions benchmark/es/error-stack.js
@@ -0,0 +1,34 @@
'use strict';

const common = require('../common.js');
const modPath = require.resolve('../fixtures/simple-error-stack.js');

const bench = common.createBenchmark(main, {
method: ['without-sourcemap', 'sourcemap'],
n: [1e5],
});

function runN(n) {
delete require.cache[modPath];
const mod = require(modPath);
bench.start();
for (let i = 0; i < n; i++) {
mod.simpleErrorStack();
}
bench.end(n);
}

function main({ n, method }) {
switch (method) {
case 'without-sourcemap':
process.setSourceMapsEnabled(false);
runN(n);
break;
case 'sourcemap':
process.setSourceMapsEnabled(true);
runN(n);
break;
default:
throw new Error(`Unexpected method "${method}"`);
}
}
15 changes: 15 additions & 0 deletions benchmark/fixtures/simple-error-stack.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions benchmark/fixtures/simple-error-stack.ts
@@ -0,0 +1,17 @@
'use strict';

// Compile with `tsc --inlineSourceMap benchmark/fixtures/simple-error-stack.ts`.

const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';

function simpleErrorStack() {
try {
(lorem as any).BANG();
} catch (e) {
return e.stack;
}
}

export {
simpleErrorStack,
};
44 changes: 26 additions & 18 deletions lib/internal/source_map/prepare_stack_trace.js
Expand Up @@ -25,6 +25,7 @@ const {
kIsNodeError,
} = require('internal/errors');
const { fileURLToPath } = require('internal/url');
const { setGetSourceMapErrorSource } = internalBinding('errors');

// Create a prettified stacktrace, inserting context from source maps
// if possible.
Expand Down Expand Up @@ -53,7 +54,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
return errorString;
}

let errorSource = '';
let lastSourceMap;
let lastFileName;
const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => {
Expand All @@ -62,14 +62,12 @@ const prepareStackTrace = (globalThis, error, trace) => {
// A stack trace will often have several call sites in a row within the
// same file, cache the source map and file content accordingly:
let fileName = t.getFileName();
let generated = false;
if (fileName === undefined) {
fileName = t.getEvalOrigin();
generated = true;
}
const sm = fileName === lastFileName ?
lastSourceMap :
findSourceMap(fileName, generated);
findSourceMap(fileName);
lastSourceMap = sm;
lastFileName = fileName;
if (sm) {
Expand All @@ -83,14 +81,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
if (originalSource && originalLine !== undefined &&
originalColumn !== undefined) {
const name = getOriginalSymbolName(sm, trace, i);
if (i === 0) {
errorSource = getErrorSource(
sm,
originalSource,
originalLine,
originalColumn
);
}
// Construct call site name based on: v8.dev/docs/stack-trace-api:
const fnName = t.getFunctionName() ?? t.getMethodName();
const typeName = t.getTypeName();
Expand All @@ -116,7 +106,7 @@ const prepareStackTrace = (globalThis, error, trace) => {
}
return `${str}${t}`;
}), '');
return `${errorSource}${errorString}\n at ${preparedTrace}`;
return `${errorString}\n at ${preparedTrace}`;
};

// Transpilers may have removed the original symbol name used in the stack
Expand Down Expand Up @@ -155,7 +145,7 @@ function getErrorSource(
fileURLToPath(originalSourcePath) : originalSourcePath;
const source = getOriginalSource(
sourceMap.payload,
originalSourcePathNoScheme
originalSourcePath
);
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
const line = lines[originalLine];
Expand All @@ -178,28 +168,46 @@ function getErrorSource(

function getOriginalSource(payload, originalSourcePath) {
let source;
const originalSourcePathNoScheme =
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
fileURLToPath(originalSourcePath) : originalSourcePath;
// payload.sources has been normalized to be an array of absolute urls.
const sourceContentIndex =
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
if (payload.sourcesContent?.[sourceContentIndex]) {
// First we check if the original source content was provided in the
// source map itself:
source = payload.sourcesContent[sourceContentIndex];
} else {
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
// If no sourcesContent was found, attempt to load the original source
// from disk:
debug(`read source of ${originalSourcePath} from filesystem`);
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
try {
source = readFileSync(originalSourcePathNoScheme, 'utf8');
} catch (err) {
debug(err);
source = '';
}
} else {
source = '';
}
return source;
}

function getSourceMapErrorSource(fileName, lineNumber, columnNumber) {
const sm = findSourceMap(fileName);
if (sm === null) {
return;
}
const {
originalLine,
originalColumn,
originalSource,
} = sm.findEntry(lineNumber - 1, columnNumber);
const errorSource = getErrorSource(sm, originalSource, originalLine, originalColumn);
return errorSource;
}

setGetSourceMapErrorSource(getSourceMapErrorSource);

module.exports = {
prepareStackTrace,
};
91 changes: 54 additions & 37 deletions lib/internal/source_map/source_map_cache.js
Expand Up @@ -41,6 +41,8 @@ const esmSourceMapCache = new SafeMap();
// The generated sources is not mutable, so we can use a Map without memory concerns:
const generatedSourceMapCache = new SafeMap();
const kLeadingProtocol = /^\w+:\/\//;
const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/;
const kSourceURLMagicComment = /\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/;

const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
let SourceMap;
Expand Down Expand Up @@ -77,7 +79,22 @@ function setSourceMapsEnabled(val) {
sourceMapsEnabled = val;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource) {
function extractSourceURLMagicComment(content) {
bcoe marked this conversation as resolved.
Show resolved Hide resolved
const matchSourceURL = RegExpPrototypeExec(
kSourceURLMagicComment,
content
);
if (matchSourceURL === null) {
return null;
}
let sourceURL = matchSourceURL.groups.sourceURL;
if (sourceURL != null && RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
return sourceURL;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
try {
Expand All @@ -87,10 +104,10 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
debug(err);
return;
}
const match = RegExpPrototypeExec(
/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/,
content,
);
const match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content);
if (sourceURL === undefined) {
sourceURL = extractSourceURLMagicComment(content);
}
if (match) {
const data = dataFromUrl(filename, match.groups.sourceMappingURL);
const url = data ? null : match.groups.sourceMappingURL;
Expand All @@ -99,22 +116,33 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
filename,
lineLengths: lineLengths(content),
data,
url
url,
sourceURL,
});
} else if (isGeneratedSource) {
generatedSourceMapCache.set(filename, {
const entry = {
lineLengths: lineLengths(content),
data,
url
});
url,
sourceURL
};
generatedSourceMapCache.set(filename, entry);
if (sourceURL) {
generatedSourceMapCache.set(sourceURL, entry);
}
} else {
// If there is no cjsModuleInstance and is not generated source assume we are in a
// "modules/esm" context.
esmSourceMapCache.set(filename, {
const entry = {
lineLengths: lineLengths(content),
data,
url
});
url,
sourceURL,
};
esmSourceMapCache.set(filename, entry);
if (sourceURL) {
esmSourceMapCache.set(sourceURL, entry);
}
}
}
}
Expand All @@ -123,19 +151,12 @@ function maybeCacheGeneratedSourceMap(content) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;

const matchSourceURL = RegExpPrototypeExec(
/\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/,
content
);
if (matchSourceURL == null) {
const sourceURL = extractSourceURLMagicComment(content);
if (sourceURL === null) {
return;
}
let sourceURL = matchSourceURL.groups.sourceURL;
if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
try {
maybeCacheSourceMap(sourceURL, content, null, true);
maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
} catch (err) {
// This can happen if the filename is not a valid URL.
// If we fail to cache the source map, we should not fail the whole process.
Expand Down Expand Up @@ -254,33 +275,29 @@ function appendCJSCache(obj) {
}
}

function findSourceMap(sourceURL, isGenerated) {
function findSourceMap(sourceURL) {
if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
if (!SourceMap) {
SourceMap = require('internal/source_map/source_map').SourceMap;
}
let sourceMap;
if (isGenerated) {
sourceMap = generatedSourceMapCache.get(sourceURL);
} else {
sourceMap = esmSourceMapCache.get(sourceURL);
if (sourceMap === undefined) {
for (const value of cjsSourceMapCache) {
const filename = ObjectGetValueSafe(value, 'filename');
if (sourceURL === filename) {
sourceMap = {
data: ObjectGetValueSafe(value, 'data')
};
}
let sourceMap = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
if (sourceMap === undefined) {
for (const value of cjsSourceMapCache) {
const filename = ObjectGetValueSafe(value, 'filename');
const cachedSourceURL = ObjectGetValueSafe(value, 'sourceURL');
if (sourceURL === filename || sourceURL === cachedSourceURL) {
sourceMap = {
data: ObjectGetValueSafe(value, 'data')
};
}
}
}
if (sourceMap && sourceMap.data) {
return new SourceMap(sourceMap.data);
}
return undefined;
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a breaking change? AVA for example relies on this that it returns undefined here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a separate issue? This looks like something unintended that we can fix.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Expand Up @@ -525,6 +525,7 @@ class NoArrayBufferZeroFillScope {
V(enhance_fatal_stack_after_inspector, v8::Function) \
V(enhance_fatal_stack_before_inspector, v8::Function) \
V(fs_use_promises_symbol, v8::Symbol) \
V(get_source_map_error_source, v8::Function) \
V(host_import_module_dynamically_callback, v8::Function) \
V(host_initialize_import_meta_object_callback, v8::Function) \
V(http2session_on_altsvc_function, v8::Function) \
Expand Down