Skip to content

Commit eee1d29

Browse files
bcoetargos
authored andcommitted
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: #35915 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 7d7e34c commit eee1d29

16 files changed

Lines changed: 281 additions & 62 deletions

‎doc/api/module.md‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,22 @@ import { findSourceMap, SourceMap } from 'module';
146146
const { findSourceMap, SourceMap } =require('module');
147147
```
148148
149-
### `module.findSourceMap(path[, error])`
149+
<!-- Anchors to make sure old links find a target -->
150+
<a id="module_module_findsourcemap_path_error"></a>
151+
### `module.findSourceMap(path)`
152+
150153
<!-- YAML
151154
added:
152155
- v13.7.0
153156
- v12.17.0
154157
-->
155158
156159
* `path` {string}
157-
* `error` {Error}
158160
* Returns: {module.SourceMap}
159161
160162
`path` is the resolved path for the file for which a corresponding source map
161163
should be fetched.
162164
163-
The `error` instance should be passed as the second parameter to `findSourceMap`
164-
in exceptional flows, such as when an overridden
165-
[`Error.prepareStackTrace(error, trace)`][] is invoked. Modules are not added to
166-
the module cache until they are successfully loaded. In these cases, source maps
167-
are associated with the `error` instance along with the `path`.
168-
169165
### Class: `module.SourceMap`
170166
<!-- YAML
171167
added:
@@ -215,7 +211,6 @@ consists of the following keys:
215211
[ES Modules]: esm.md
216212
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
217213
[`--enable-source-maps`]: cli.md#cli_enable_source_maps
218-
[`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
219214
[`NODE_V8_COVERAGE=dir`]: cli.md#cli_node_v8_coverage_dir
220215
[`SourceMap`]: #module_class_module_sourcemap
221216
[`createRequire()`]: #module_module_createrequire_filename

‎doc/api/modules.md‎

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

978978
<!-- Anchors to make sure old links find a target -->
979-
* <aid="modules_module_findsourcemap_path_error"href="module.html#module_module_findsourcemap_path_error">`module.findSourceMap(path[, error])`</a>
979+
* <aid="modules_module_findsourcemap_path_error"href="module.html#module_module_findsourcemap_path">`module.findSourceMap(path)`</a>
980980
* <aid="modules_class_module_sourcemap"href="module.html#module_class_module_sourcemap">Class: `module.SourceMap`</a>
981981
* <aid="modules_new_sourcemap_payload"href="module.html#module_new_sourcemap_payload">`new SourceMap(payload)`</a>
982982
* <aid="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
returnsafe;
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const prepareStackTrace = (globalThis, error, trace) => {
5757
letstr=i!==0 ? '\n at ' : '';
5858
str=`${str}${t}`;
5959
try{
60-
constsm=findSourceMap(t.getFileName(),error);
60+
constsm=findSourceMap(t.getFileName());
6161
if(sm){
6262
// Source Map V3 lines/columns use zero-based offsets whereas, in
6363
// stack traces, they start at 1/1.
@@ -119,6 +119,7 @@ function getErrorSource(payload, originalSource, firstLine, firstColumn) {
119119
source=readFileSync(originalSourceNoScheme,'utf8');
120120
}catch(err){
121121
debug(err);
122+
return'';
122123
}
123124
}
124125

‎lib/internal/source_map/source_map_cache.js‎

Lines changed: 38 additions & 50 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

@@ -27,17 +30,17 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
2730
});
2831
constfs=require('fs');
2932
const{ getOptionValue }=require('internal/options');
33+
const{ IterableWeakMap }=require('internal/util/iterable_weak_map');
3034
const{
3135
normalizeReferrerURL,
3236
}=require('internal/modules/cjs/helpers');
33-
// For cjs, since Module._cache is exposed to users, we use a WeakMap
34-
// keyed on module, facilitating garbage collection.
35-
constcjsSourceMapCache=newWeakMap();
36-
// The esm cache is not exposed to users, so we can use a Map keyed
37-
// on filenames.
38-
constesmSourceMapCache=newMap();
39-
const{ fileURLToPath,URL}=require('url');
40-
letModule;
37+
// Since the CJS module cache is mutable, which leads to memory leaks when
38+
// modules are deleted, we use a WeakMap so that the source map cache will
39+
// be purged automatically:
40+
constcjsSourceMapCache=newIterableWeakMap();
41+
// The esm cache is not mutable, so we can use a Map without memory concerns:
42+
constesmSourceMapCache=newSafeMap();
43+
const{ fileURLToPath, pathToFileURL,URL}=require('internal/url');
4144
letSourceMap;
4245

4346
letsourceMapsEnabled;
@@ -70,13 +73,14 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
7073
debug(err.stack);
7174
return;
7275
}
73-
74-
constmatch=content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
76+
constmatch=StringPrototypeMatch(
77+
content,
78+
/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/
79+
);
7580
if(match){
7681
constdata=dataFromUrl(filename,match.groups.sourceMappingURL);
7782
consturl=data ? null : match.groups.sourceMappingURL;
7883
if(cjsModuleInstance){
79-
if(!Module)Module=require('internal/modules/cjs/loader').Module;
8084
cjsSourceMapCache.set(cjsModuleInstance,{
8185
filename,
8286
lineLengths: lineLengths(content),
@@ -120,7 +124,7 @@ function lineLengths(content) {
120124
// We purposefully keep \r as part of the line-length calculation, in
121125
// cases where there is a \r\n separator, so that this can be taken into
122126
// account in coverage calculations.
123-
returncontent.split(/\n|\u2028|\u2029/).map((line)=>{
127+
returnArrayPrototypeMap(StringPrototypeSplit(content,/\n|\u2028|\u2029/),(line)=>{
124128
returnline.length;
125129
});
126130
}
@@ -139,8 +143,8 @@ function sourceMapFromFile(mapURL) {
139143
// data:[<mediatype>][;base64],<data> see:
140144
// https://tools.ietf.org/html/rfc2397#section-2
141145
functionsourceMapFromDataUrl(sourceURL,url){
142-
const[format,data]=url.split(',');
143-
constsplitFormat=format.split(';');
146+
const[format,data]=StringPrototypeSplit(url,',');
147+
constsplitFormat=StringPrototypeSplit(format,';');
144148
constcontentType=splitFormat[0];
145149
constbase64=splitFormat[splitFormat.length-1]==='base64';
146150
if(contentType==='application/json'){
@@ -207,48 +211,32 @@ function sourceMapCacheToObject() {
207211
returnobj;
208212
}
209213

210-
// Since WeakMap can't be iterated over, we use Module._cache's
211-
// keys to facilitate Source Map serialization.
212-
//
213-
// TODO(bcoe): this means we don't currently serialize source-maps attached
214-
// to error instances, only module instances.
215214
functionappendCJSCache(obj){
216-
if(!Module)return;
217-
constcjsModuleCache=ObjectGetValueSafe(Module,'_cache');
218-
constcjsModules=ObjectKeys(cjsModuleCache);
219-
for(leti=0;i<cjsModules.length;i++){
220-
constkey=cjsModules[i];
221-
constmodule=ObjectGetValueSafe(cjsModuleCache,key);
222-
constvalue=WeakMapPrototypeGet(cjsSourceMapCache,module);
223-
if(value){
224-
// This is okay because `obj` has a null prototype.
225-
obj[`file://${key}`]={
226-
lineLengths: ObjectGetValueSafe(value,'lineLengths'),
227-
data: ObjectGetValueSafe(value,'data'),
228-
url: ObjectGetValueSafe(value,'url')
229-
};
230-
}
215+
for(constvalueofcjsSourceMapCache){
216+
obj[ObjectGetValueSafe(value,'filename')]={
217+
lineLengths: ObjectGetValueSafe(value,'lineLengths'),
218+
data: ObjectGetValueSafe(value,'data'),
219+
url: ObjectGetValueSafe(value,'url')
220+
};
231221
}
232222
}
233223

234-
// Attempt to lookup a source map, which is either attached to a file URI, or
235-
// keyed on an error instance.
236-
// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
237-
// requirement of error parameter.
238-
functionfindSourceMap(uri,error){
239-
if(!Module)Module=require('internal/modules/cjs/loader').Module;
224+
functionfindSourceMap(sourceURL){
225+
if(!RegExpPrototypeTest(/^\w+:\/\//,sourceURL)){
226+
sourceURL=pathToFileURL(sourceURL).href;
227+
}
240228
if(!SourceMap){
241229
SourceMap=require('internal/source_map/source_map').SourceMap;
242230
}
243-
letsourceMap=cjsSourceMapCache.get(Module._cache[uri]);
244-
if(!uri.startsWith('file://'))uri=normalizeReferrerURL(uri);
245-
if(sourceMap===undefined){
246-
sourceMap=esmSourceMapCache.get(uri);
247-
}
231+
letsourceMap=esmSourceMapCache.get(sourceURL);
248232
if(sourceMap===undefined){
249-
constcandidateSourceMap=cjsSourceMapCache.get(error);
250-
if(candidateSourceMap&&uri===candidateSourceMap.filename){
251-
sourceMap=candidateSourceMap;
233+
for(constvalueofcjsSourceMapCache){
234+
constfilename=ObjectGetValueSafe(value,'filename');
235+
if(sourceURL===filename){
236+
sourceMap={
237+
data: ObjectGetValueSafe(value,'data')
238+
};
239+
}
252240
}
253241
}
254242
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+
constSafeFinalizationRegistry=makeSafe(
13+
globalThis.FinalizationRegistry,
14+
classSafeFinalizationRegistryextendsglobalThis.FinalizationRegistry{}
15+
);
16+
17+
// TODO(aduh95): Add WeakRef to primordials
18+
constSafeWeakRef=makeSafe(
19+
globalThis.WeakRef,
20+
classSafeWeakRefextendsglobalThis.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+
classIterableWeakMap{
28+
#weakMap =newSafeWeakMap();
29+
#refSet =newSafeSet();
30+
#finalizationGroup =newSafeFinalizationRegistry(cleanup);
31+
32+
set(key,value){
33+
constentry=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+
constref=newSafeWeakRef(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+
returnthis.#weakMap.get(key)?.value;
51+
}
52+
53+
has(key){
54+
returnthis.#weakMap.has(key);
55+
}
56+
57+
delete(key){
58+
constentry=this.#weakMap.get(key);
59+
if(!entry){
60+
returnfalse;
61+
}
62+
this.#weakMap.delete(key);
63+
this.#refSet.delete(entry.ref);
64+
this.#finalizationGroup.unregister(entry.ref);
65+
returntrue;
66+
}
67+
68+
*[SymbolIterator](){
69+
for(constrefofthis.#refSet){
70+
constkey=ref.deref();
71+
if(!key)continue;
72+
const{ value }=this.#weakMap.get(key);
73+
yieldvalue;
74+
}
75+
}
76+
}
77+
78+
functioncleanup({ 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
@@ -211,6 +211,7 @@
211211
'lib/internal/util/debuglog.js',
212212
'lib/internal/util/inspect.js',
213213
'lib/internal/util/inspector.js',
214+
'lib/internal/util/iterable_weak_map.js',
214215
'lib/internal/util/types.js',
215216
'lib/internal/http2/core.js',
216217
'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.

0 commit comments

Comments
 (0)