Skip to content

Commit 2197404

Browse files
committed
module: refactor to use iterable-weak-map
Using an iterable WeakMap (a data-structure that uses WeakRef and WeakMap), we are able to: stop relying on Module._cache to serialize source maps; stop requiring an error object when calling findSourceMap(). PR-URL: nodejs#35915 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 6562506 commit 2197404

16 files changed

+313
-68
lines changed

doc/api/module.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,22 @@ import { findSourceMap, SourceMap } from 'module';
135135
const { findSourceMap, SourceMap } = require('module');
136136
```
137137
138-
### `module.findSourceMap(path[, error])`
138+
<!-- Anchors to make sure old links find a target -->
139+
<a id="module_module_findsourcemap_path_error"></a>
140+
### `module.findSourceMap(path)`
141+
139142
<!-- YAML
140143
added:
141144
- v13.7.0
142145
- v12.17.0
143146
-->
144147
145148
* `path` {string}
146-
* `error` {Error}
147149
* Returns: {module.SourceMap}
148150
149151
`path` is the resolved path for the file for which a corresponding source map
150152
should be fetched.
151153
152-
The `error` instance should be passed as the second parameter to `findSourceMap`
153-
in exceptional flows, such as when an overridden
154-
[`Error.prepareStackTrace(error, trace)`][] is invoked. Modules are not added to
155-
the module cache until they are successfully loaded. In these cases, source maps
156-
are associated with the `error` instance along with the `path`.
157-
158154
### Class: `module.SourceMap`
159155
<!-- YAML
160156
added:
@@ -204,7 +200,6 @@ consists of the following keys:
204200
[ES Modules]: esm.md
205201
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
206202
[`--enable-source-maps`]: cli.md#cli_enable_source_maps
207-
[`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
208203
[`NODE_V8_COVERAGE=dir`]: cli.md#cli_node_v8_coverage_dir
209204
[`SourceMap`]: #module_class_module_sourcemap
210205
[`createRequire()`]: #module_module_createrequire_filename

doc/api/modules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ This section was moved to
968968
[Modules: `module` core module](module.md#module_source_map_v3_support).
969969

970970
<!-- Anchors to make sure old links find a target -->
971-
* <a id="modules_module_findsourcemap_path_error" href="module.html#module_module_findsourcemap_path_error">`module.findSourceMap(path[, error])`</a>
971+
* <a id="modules_module_findsourcemap_path_error" href="module.html#module_module_findsourcemap_path">`module.findSourceMap(path)`</a>
972972
* <a id="modules_class_module_sourcemap" href="module.html#module_class_module_sourcemap">Class: `module.SourceMap`</a>
973973
* <a id="modules_new_sourcemap_payload" href="module.html#module_new_sourcemap_payload">`new SourceMap(payload)`</a>
974974
* <a id="modules_sourcemap_payload" href="module.html#module_sourcemap_payload">`sourceMap.payload`</a>

lib/internal/per_context/primordials.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ function makeSafe(unsafe, safe) {
8686
Object.freeze(safe);
8787
return safe;
8888
}
89+
primordials.makeSafe = makeSafe;
8990

9091
// Subclass the constructors because we need to use their prototype
9192
// methods later.

lib/internal/source_map/prepare_stack_trace.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const prepareStackTrace = (globalThis, error, trace) => {
5252
let str = i !== 0 ? '\n at ' : '';
5353
str = `${str}${t}`;
5454
try {
55-
const sm = findSourceMap(t.getFileName(), error);
55+
const sm = findSourceMap(t.getFileName());
5656
if (sm) {
5757
// Source Map V3 lines/columns use zero-based offsets whereas, in
5858
// stack traces, they start at 1/1.
@@ -91,11 +91,21 @@ const prepareStackTrace = (globalThis, error, trace) => {
9191
function getErrorSource(firstSource, firstLine, firstColumn) {
9292
let exceptionLine = '';
9393
let source;
94-
try {
95-
source = readFileSync(firstSource, 'utf8');
96-
} catch (err) {
97-
debug(err);
98-
return exceptionLine;
94+
const sourceContentIndex =
95+
ArrayPrototypeIndexOf(payload.sources, originalSource);
96+
if (payload.sourcesContent?.[sourceContentIndex]) {
97+
// First we check if the original source content was provided in the
98+
// source map itself:
99+
source = payload.sourcesContent[sourceContentIndex];
100+
} else {
101+
// If no sourcesContent was found, attempt to load the original source
102+
// from disk:
103+
try {
104+
source = readFileSync(originalSourceNoScheme, 'utf8');
105+
} catch (err) {
106+
debug(err);
107+
return '';
108+
}
99109
}
100110
const lines = source.split(/\r?\n/, firstLine);
101111
const line = lines[firstLine - 1];

lib/internal/source_map/source_map_cache.js

Lines changed: 39 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeMap,
45
JSONParse,
56
ObjectCreate,
67
ObjectKeys,
78
ObjectGetOwnPropertyDescriptor,
89
ObjectPrototypeHasOwnProperty,
910
Map,
1011
MapPrototypeEntries,
11-
WeakMap,
12-
WeakMapPrototypeGet,
12+
RegExpPrototypeTest,
13+
SafeMap,
14+
StringPrototypeMatch,
15+
StringPrototypeSplit,
1316
uncurryThis,
1417
} = primordials;
1518

@@ -28,17 +31,17 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
2831
const { dirname, resolve } = require('path');
2932
const fs = require('fs');
3033
const { getOptionValue } = require('internal/options');
34+
const { IterableWeakMap } = require('internal/util/iterable_weak_map');
3135
const {
3236
normalizeReferrerURL,
3337
} = require('internal/modules/cjs/helpers');
34-
// For cjs, since Module._cache is exposed to users, we use a WeakMap
35-
// keyed on module, facilitating garbage collection.
36-
const cjsSourceMapCache = new WeakMap();
37-
// The esm cache is not exposed to users, so we can use a Map keyed
38-
// on filenames.
39-
const esmSourceMapCache = new Map();
40-
const { fileURLToPath, URL } = require('url');
41-
let Module;
38+
// Since the CJS module cache is mutable, which leads to memory leaks when
39+
// modules are deleted, we use a WeakMap so that the source map cache will
40+
// be purged automatically:
41+
const cjsSourceMapCache = new IterableWeakMap();
42+
// The esm cache is not mutable, so we can use a Map without memory concerns:
43+
const esmSourceMapCache = new SafeMap();
44+
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
4245
let SourceMap;
4346

4447
let sourceMapsEnabled;
@@ -73,13 +76,14 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
7376
debug(err.stack);
7477
return;
7578
}
76-
77-
const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
79+
const match = StringPrototypeMatch(
80+
content,
81+
/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/
82+
);
7883
if (match) {
7984
const data = dataFromUrl(basePath, match.groups.sourceMappingURL);
8085
const url = data ? null : match.groups.sourceMappingURL;
8186
if (cjsModuleInstance) {
82-
if (!Module) Module = require('internal/modules/cjs/loader').Module;
8387
cjsSourceMapCache.set(cjsModuleInstance, {
8488
filename,
8589
lineLengths: lineLengths(content),
@@ -123,7 +127,7 @@ function lineLengths(content) {
123127
// We purposefully keep \r as part of the line-length calculation, in
124128
// cases where there is a \r\n separator, so that this can be taken into
125129
// account in coverage calculations.
126-
return content.split(/\n|\u2028|\u2029/).map((line) => {
130+
return ArrayPrototypeMap(StringPrototypeSplit(content, /\n|\u2028|\u2029/), (line) => {
127131
return line.length;
128132
});
129133
}
@@ -141,9 +145,9 @@ function sourceMapFromFile(sourceMapFile) {
141145

142146
// data:[<mediatype>][;base64],<data> see:
143147
// https://tools.ietf.org/html/rfc2397#section-2
144-
function sourceMapFromDataUrl(basePath, url) {
145-
const [format, data] = url.split(',');
146-
const splitFormat = format.split(';');
148+
function sourceMapFromDataUrl(sourceURL, url) {
149+
const [format, data] = StringPrototypeSplit(url, ',');
150+
const splitFormat = StringPrototypeSplit(format, ';');
147151
const contentType = splitFormat[0];
148152
const base64 = splitFormat[splitFormat.length - 1] === 'base64';
149153
if (contentType === 'application/json') {
@@ -214,48 +218,32 @@ function sourceMapCacheToObject() {
214218
return obj;
215219
}
216220

217-
// Since WeakMap can't be iterated over, we use Module._cache's
218-
// keys to facilitate Source Map serialization.
219-
//
220-
// TODO(bcoe): this means we don't currently serialize source-maps attached
221-
// to error instances, only module instances.
222221
function appendCJSCache(obj) {
223-
if (!Module) return;
224-
const cjsModuleCache = ObjectGetValueSafe(Module, '_cache');
225-
const cjsModules = ObjectKeys(cjsModuleCache);
226-
for (let i = 0; i < cjsModules.length; i++) {
227-
const key = cjsModules[i];
228-
const module = ObjectGetValueSafe(cjsModuleCache, key);
229-
const value = WeakMapPrototypeGet(cjsSourceMapCache, module);
230-
if (value) {
231-
// This is okay because `obj` has a null prototype.
232-
obj[`file://${key}`] = {
233-
lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
234-
data: ObjectGetValueSafe(value, 'data'),
235-
url: ObjectGetValueSafe(value, 'url')
236-
};
237-
}
222+
for (const value of cjsSourceMapCache) {
223+
obj[ObjectGetValueSafe(value, 'filename')] = {
224+
lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
225+
data: ObjectGetValueSafe(value, 'data'),
226+
url: ObjectGetValueSafe(value, 'url')
227+
};
238228
}
239229
}
240230

241-
// Attempt to lookup a source map, which is either attached to a file URI, or
242-
// keyed on an error instance.
243-
// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
244-
// requirement of error parameter.
245-
function findSourceMap(uri, error) {
246-
if (!Module) Module = require('internal/modules/cjs/loader').Module;
231+
function findSourceMap(sourceURL) {
232+
if (!RegExpPrototypeTest(/^\w+:\/\//, sourceURL)) {
233+
sourceURL = pathToFileURL(sourceURL).href;
234+
}
247235
if (!SourceMap) {
248236
SourceMap = require('internal/source_map/source_map').SourceMap;
249237
}
250-
let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
251-
if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
252-
if (sourceMap === undefined) {
253-
sourceMap = esmSourceMapCache.get(uri);
254-
}
238+
let sourceMap = esmSourceMapCache.get(sourceURL);
255239
if (sourceMap === undefined) {
256-
const candidateSourceMap = cjsSourceMapCache.get(error);
257-
if (candidateSourceMap && uri === candidateSourceMap.filename) {
258-
sourceMap = candidateSourceMap;
240+
for (const value of cjsSourceMapCache) {
241+
const filename = ObjectGetValueSafe(value, 'filename');
242+
if (sourceURL === filename) {
243+
sourceMap = {
244+
data: ObjectGetValueSafe(value, 'data')
245+
};
246+
}
259247
}
260248
}
261249
if (sourceMap && sourceMap.data) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const {
4+
makeSafe,
5+
Object,
6+
SafeSet,
7+
SafeWeakMap,
8+
SymbolIterator,
9+
} = primordials;
10+
11+
// TODO(aduh95): Add FinalizationRegistry to primordials
12+
const SafeFinalizationRegistry = makeSafe(
13+
globalThis.FinalizationRegistry,
14+
class SafeFinalizationRegistry extends globalThis.FinalizationRegistry {}
15+
);
16+
17+
// TODO(aduh95): Add WeakRef to primordials
18+
const SafeWeakRef = makeSafe(
19+
globalThis.WeakRef,
20+
class SafeWeakRef extends globalThis.WeakRef {}
21+
);
22+
23+
// This class is modified from the example code in the WeakRefs specification:
24+
// https://github.com/tc39/proposal-weakrefs
25+
// Licensed under ECMA's MIT-style license, see:
26+
// https://github.com/tc39/ecma262/blob/master/LICENSE.md
27+
class IterableWeakMap {
28+
#weakMap = new SafeWeakMap();
29+
#refSet = new SafeSet();
30+
#finalizationGroup = new SafeFinalizationRegistry(cleanup);
31+
32+
set(key, value) {
33+
const entry = this.#weakMap.get(key);
34+
if (entry) {
35+
// If there's already an entry for the object represented by "key",
36+
// the value can be updated without creating a new WeakRef:
37+
this.#weakMap.set(key, { value, ref: entry.ref });
38+
} else {
39+
const ref = new SafeWeakRef(key);
40+
this.#weakMap.set(key, { value, ref });
41+
this.#refSet.add(ref);
42+
this.#finalizationGroup.register(key, {
43+
set: this.#refSet,
44+
ref
45+
}, ref);
46+
}
47+
}
48+
49+
get(key) {
50+
return this.#weakMap.get(key)?.value;
51+
}
52+
53+
has(key) {
54+
return this.#weakMap.has(key);
55+
}
56+
57+
delete(key) {
58+
const entry = this.#weakMap.get(key);
59+
if (!entry) {
60+
return false;
61+
}
62+
this.#weakMap.delete(key);
63+
this.#refSet.delete(entry.ref);
64+
this.#finalizationGroup.unregister(entry.ref);
65+
return true;
66+
}
67+
68+
*[SymbolIterator]() {
69+
for (const ref of this.#refSet) {
70+
const key = ref.deref();
71+
if (!key) continue;
72+
const { value } = this.#weakMap.get(key);
73+
yield value;
74+
}
75+
}
76+
}
77+
78+
function cleanup({ set, ref }) {
79+
set.delete(ref);
80+
}
81+
82+
Object.freeze(IterableWeakMap.prototype);
83+
84+
module.exports = {
85+
IterableWeakMap,
86+
};

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
'lib/internal/util/debuglog.js',
209209
'lib/internal/util/inspect.js',
210210
'lib/internal/util/inspector.js',
211+
'lib/internal/util/iterable_weak_map.js',
211212
'lib/internal/util/types.js',
212213
'lib/internal/http2/core.js',
213214
'lib/internal/http2/compat.js',

test/fixtures/source-map/throw-on-require-entry.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/source-map/throw-on-require-entry.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/source-map/throw-on-require.js

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/source-map/throw-on-require.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
enum ATrue {
2+
IsTrue = 1,
3+
IsFalse = 0
4+
}
5+
6+
if (false) {
7+
console.info('unreachable')
8+
} else if (true) {
9+
throw Error('throw early')
10+
} else {
11+
console.info('unreachable')
12+
}

test/parallel/test-bootstrap-modules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const expectedModules = new Set([
8080
'NativeModule internal/util',
8181
'NativeModule internal/util/debuglog',
8282
'NativeModule internal/util/inspect',
83+
'NativeModule internal/util/iterable_weak_map',
8384
'NativeModule internal/util/types',
8485
'NativeModule internal/validators',
8586
'NativeModule internal/vm/module',

0 commit comments

Comments
 (0)