Skip to content

Commit fe007cd

Browse files
legendecastargos
authored andcommitted
lib: allow CJS source map cache to be reclaimed
Unifies the CJS and ESM source map cache map with SourceMapCacheMap and allows the CJS cache entries to be queried more efficiently with a source url without iteration on an IterableWeakMap. Add a test to verify that the CJS source map cache entry can be reclaimed. PR-URL: #51711 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 374743c commit fe007cd

16 files changed

Lines changed: 393 additions & 277 deletions

‎lib/internal/modules/cjs/loader.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,7 +1335,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
13351335
// Cache the source map for the module if present.
13361336
const{ sourceMapURL }=script;
13371337
if(sourceMapURL){
1338-
maybeCacheSourceMap(filename,content,this,false,undefined,sourceMapURL);
1338+
maybeCacheSourceMap(filename,content,cjsModuleInstance,false,undefined,sourceMapURL);
13391339
}
13401340

13411341
return{
@@ -1358,7 +1358,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
13581358

13591359
// Cache the source map for the module if present.
13601360
if(result.sourceMapURL){
1361-
maybeCacheSourceMap(filename,content,this,false,undefined,result.sourceMapURL);
1361+
maybeCacheSourceMap(filename,content,cjsModuleInstance,false,undefined,result.sourceMapURL);
13621362
}
13631363

13641364
returnresult;

‎lib/internal/modules/esm/translators.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,12 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
178178
functionloadCJSModule(module,source,url,filename,isMain){
179179
constcompileResult=compileFunctionForCJSLoader(source,filename,isMain,false);
180180

181+
const{function: compiledWrapper, sourceMapURL }=compileResult;
181182
// Cache the source map for the cjs module if present.
182-
if(compileResult.sourceMapURL){
183-
maybeCacheSourceMap(url,source,null,false,undefined,compileResult.sourceMapURL);
183+
if(sourceMapURL){
184+
maybeCacheSourceMap(url,source,module,false,undefined,sourceMapURL);
184185
}
185186

186-
constcompiledWrapper=compileResult.function;
187-
188187
constcascadedLoader=require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
189188
const__dirname=dirname(filename);
190189
// eslint-disable-next-line func-name-matching,func-style

‎lib/internal/modules/esm/utils.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ function compileSourceTextModule(url, source, cascadedLoader) {
341341
}
342342
// Cache the source map for the module if present.
343343
if(wrap.sourceMapURL){
344-
maybeCacheSourceMap(url,source,null,false,undefined,wrap.sourceMapURL);
344+
maybeCacheSourceMap(url,source,wrap,false,undefined,wrap.sourceMapURL);
345345
}
346346
returnwrap;
347347
}

‎lib/internal/source_map/prepare_stack_trace.js‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,15 @@ function getOriginalSymbolName(sourceMap, trace, curIndex) {
114114
}
115115
}
116116

117-
// Places a snippet of code from where the exception was originally thrown
118-
// above the stack trace. This logic is modeled after GetErrorSource in
119-
// node_errors.cc.
117+
/**
118+
* Return a snippet of code from where the exception was originally thrown
119+
* above the stack trace. This called from GetErrorSource in node_errors.cc.
120+
* @param {import('internal/source_map/source_map').SourceMap} sourceMap - the source map to be used
121+
* @param {string} originalSourcePath - path or url of the original source
122+
* @param {number} originalLine - line number in the original source
123+
* @param {number} originalColumn - column number in the original source
124+
* @returns {string | undefined} - the exact line in the source content or undefined if file not found
125+
*/
120126
functiongetErrorSource(
121127
sourceMap,
122128
originalSourcePath,
@@ -154,6 +160,12 @@ function getErrorSource(
154160
returnexceptionLine;
155161
}
156162

163+
/**
164+
* Retrieve the original source code from the source map's `sources` list or disk.
165+
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
166+
* @param {string} originalSourcePath - path or url of the original source
167+
* @returns {string | undefined} - the source content or undefined if file not found
168+
*/
157169
functiongetOriginalSource(payload,originalSourcePath){
158170
letsource;
159171
// payload.sources has been normalized to be an array of absolute urls.
@@ -177,6 +189,13 @@ function getOriginalSource(payload, originalSourcePath) {
177189
returnsource;
178190
}
179191

192+
/**
193+
* Retrieve exact line in the original source code from the source map's `sources` list or disk.
194+
* @param {string} fileName - actual file name
195+
* @param {number} lineNumber - actual line number
196+
* @param {number} columnNumber - actual column number
197+
* @returns {string | undefined} - the source content or undefined if file not found
198+
*/
180199
functiongetSourceMapErrorSource(fileName,lineNumber,columnNumber){
181200
constsm=findSourceMap(fileName);
182201
if(sm===undefined){

‎lib/internal/source_map/source_map_cache.js‎

Lines changed: 89 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const{
44
ArrayPrototypePush,
55
JSONParse,
6-
ObjectKeys,
76
RegExpPrototypeExec,
87
SafeMap,
98
StringPrototypeCodePointAt,
@@ -25,17 +24,15 @@ const {
2524
}=require('internal/errors');
2625
const{ getLazy }=require('internal/util');
2726

28-
// Since the CJS module cache is mutable, which leads to memory leaks when
29-
// modules are deleted, we use a WeakMap so that the source map cache will
30-
// be purged automatically:
31-
constgetCjsSourceMapCache=getLazy(()=>{
32-
const{ IterableWeakMap }=require('internal/util/iterable_weak_map');
33-
returnnewIterableWeakMap();
27+
constgetModuleSourceMapCache=getLazy(()=>{
28+
const{ SourceMapCacheMap }=require('internal/source_map/source_map_cache_map');
29+
returnnewSourceMapCacheMap();
3430
});
3531

36-
// The esm cache is not mutable, so we can use a Map without memory concerns:
37-
constesmSourceMapCache=newSafeMap();
38-
// The generated sources is not mutable, so we can use a Map without memory concerns:
32+
// The generated source module/script instance is not accessible, so we can use
33+
// a Map without memory concerns. Separate generated source entries with the module
34+
// source entries to avoid overriding the module source entries with arbitrary
35+
// source url magic comments.
3936
constgeneratedSourceMapCache=newSafeMap();
4037
constkLeadingProtocol=/^\w+:\/\//;
4138
constkSourceMappingURLMagicComment=/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/g;
@@ -52,6 +49,10 @@ function getSourceMapsEnabled() {
5249
returnsourceMapsEnabled;
5350
}
5451

52+
/**
53+
* Enables or disables source maps programmatically.
54+
* @param {boolean} val
55+
*/
5556
functionsetSourceMapsEnabled(val){
5657
validateBoolean(val,'val');
5758

@@ -72,6 +73,14 @@ function setSourceMapsEnabled(val) {
7273
sourceMapsEnabled=val;
7374
}
7475

76+
/**
77+
* Extracts the source url from the content if present. For example
78+
* //# sourceURL=file:///path/to/file
79+
*
80+
* Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code
81+
* @param {string} content - source content
82+
* @returns {string | null} source url or null if not present
83+
*/
7584
functionextractSourceURLMagicComment(content){
7685
letmatch;
7786
letmatchSourceURL;
@@ -90,6 +99,14 @@ function extractSourceURLMagicComment(content) {
9099
returnsourceURL;
91100
}
92101

102+
/**
103+
* Extracts the source map url from the content if present. For example
104+
* //# sourceMappingURL=file:///path/to/file
105+
*
106+
* Read more at: https://tc39.es/source-map-spec/#linking-generated-code
107+
* @param {string} content - source content
108+
* @returns {string | null} source map url or null if not present
109+
*/
93110
functionextractSourceMapURLMagicComment(content){
94111
letmatch;
95112
letlastMatch;
@@ -104,7 +121,17 @@ function extractSourceMapURLMagicComment(content) {
104121
returnlastMatch.groups.sourceMappingURL;
105122
}
106123

107-
functionmaybeCacheSourceMap(filename,content,cjsModuleInstance,isGeneratedSource,sourceURL,sourceMapURL){
124+
/**
125+
* Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL.
126+
* @param {string} filename - the actual filename
127+
* @param {string} content - the actual source content
128+
* @param {import('internal/modules/cjs/loader').Module | ModuleWrap} moduleInstance - a module instance that
129+
* associated with the source, once this is reclaimed, the source map entry will be removed from the cache
130+
* @param {boolean} isGeneratedSource - if the source was generated and evaluated with the global eval
131+
* @param {string | undefined} sourceURL - the source url
132+
* @param {string | undefined} sourceMapURL - the source map url
133+
*/
134+
functionmaybeCacheSourceMap(filename,content,moduleInstance,isGeneratedSource,sourceURL,sourceMapURL){
108135
constsourceMapsEnabled=getSourceMapsEnabled();
109136
if(!(process.env.NODE_V8_COVERAGE||sourceMapsEnabled))return;
110137
const{ normalizeReferrerURL }=require('internal/modules/helpers');
@@ -130,45 +157,32 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
130157
}
131158

132159
constdata=dataFromUrl(filename,sourceMapURL);
133-
consturl=data ? null : sourceMapURL;
134-
if(cjsModuleInstance){
135-
getCjsSourceMapCache().set(cjsModuleInstance,{
136-
__proto__: null,
137-
filename,
138-
lineLengths: lineLengths(content),
139-
data,
140-
url,
141-
sourceURL,
142-
});
143-
}elseif(isGeneratedSource){
144-
constentry={
145-
__proto__: null,
146-
lineLengths: lineLengths(content),
147-
data,
148-
url,
149-
sourceURL,
150-
};
160+
constentry={
161+
__proto__: null,
162+
lineLengths: lineLengths(content),
163+
data,
164+
// Save the source map url if it is not a data url.
165+
sourceMapURL: data ? null : sourceMapURL,
166+
sourceURL,
167+
};
168+
169+
if(isGeneratedSource){
151170
generatedSourceMapCache.set(filename,entry);
152171
if(sourceURL){
153172
generatedSourceMapCache.set(sourceURL,entry);
154173
}
155-
}else{
156-
// If there is no cjsModuleInstance and is not generated source assume we are in a
157-
// "modules/esm" context.
158-
constentry={
159-
__proto__: null,
160-
lineLengths: lineLengths(content),
161-
data,
162-
url,
163-
sourceURL,
164-
};
165-
esmSourceMapCache.set(filename,entry);
166-
if(sourceURL){
167-
esmSourceMapCache.set(sourceURL,entry);
168-
}
174+
return;
169175
}
176+
// If it is not a generated source, we assume we are in a "cjs/esm"
177+
// context.
178+
constkeys=sourceURL ? [filename,sourceURL] : [filename];
179+
getModuleSourceMapCache().set(keys,entry,moduleInstance);
170180
}
171181

182+
/**
183+
* Caches the source map if it is present in the eval'd source.
184+
* @param {string} content - the eval'd source code
185+
*/
172186
functionmaybeCacheGeneratedSourceMap(content){
173187
constsourceMapsEnabled=getSourceMapsEnabled();
174188
if(!(process.env.NODE_V8_COVERAGE||sourceMapsEnabled))return;
@@ -186,6 +200,14 @@ function maybeCacheGeneratedSourceMap(content) {
186200
}
187201
}
188202

203+
/**
204+
* Resolves source map payload data from the source url and source map url.
205+
* If the source map url is a data url, the data is returned.
206+
* Otherwise the source map url is resolved to a file path and the file is read.
207+
* @param {string} sourceURL - url of the source file
208+
* @param {string} sourceMappingURL - url of the source map
209+
* @returns {object} deserialized source map JSON object
210+
*/
189211
functiondataFromUrl(sourceURL,sourceMappingURL){
190212
try{
191213
consturl=newURL(sourceMappingURL);
@@ -227,7 +249,11 @@ function lineLengths(content) {
227249
returnoutput;
228250
}
229251

230-
252+
/**
253+
* Read source map from file.
254+
* @param {string} mapURL - file url of the source map
255+
* @returns {object} deserialized source map JSON object
256+
*/
231257
functionsourceMapFromFile(mapURL){
232258
try{
233259
constfs=require('fs');
@@ -281,56 +307,44 @@ function sourcesToAbsolute(baseURL, data) {
281307
returndata;
282308
}
283309

284-
// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
285-
// shutdown. In particular, they also run when Workers are terminated, making
286-
// it important that they do not call out to any user-provided code, including
287-
// built-in prototypes that might have been tampered with.
310+
// WARNING: The `sourceMapCacheToObject` runs during shutdown. In particular,
311+
// it also runs when Workers are terminated, making it important that it does
312+
// not call out to any user-provided code, including built-in prototypes that
313+
// might have been tampered with.
288314

289315
// Get serialized representation of source-map cache, this is used
290316
// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
291317
functionsourceMapCacheToObject(){
292-
constobj={__proto__: null};
293-
294-
for(const{0: k,1: v}ofesmSourceMapCache){
295-
obj[k]=v;
296-
}
297-
298-
appendCJSCache(obj);
299-
300-
if(ObjectKeys(obj).length===0){
318+
constmoduleSourceMapCache=getModuleSourceMapCache();
319+
if(moduleSourceMapCache.size===0){
301320
returnundefined;
302321
}
303-
returnobj;
304-
}
305322

306-
functionappendCJSCache(obj){
307-
for(constvalueofgetCjsSourceMapCache()){
308-
obj[value.filename]={
323+
constobj={__proto__: null};
324+
for(const{0: k,1: v}ofmoduleSourceMapCache){
325+
obj[k]={
309326
__proto__: null,
310-
lineLengths: value.lineLengths,
311-
data: value.data,
312-
url: value.url,
327+
lineLengths: v.lineLengths,
328+
data: v.data,
329+
url: v.sourceMapURL,
313330
};
314331
}
332+
returnobj;
315333
}
316334

335+
/**
336+
* Find a source map for a given actual source URL or path.
337+
* @param {string} sourceURL - actual source URL or path
338+
* @returns {import('internal/source_map/source_map').SourceMap | undefined} a source map or undefined if not found
339+
*/
317340
functionfindSourceMap(sourceURL){
318341
if(RegExpPrototypeExec(kLeadingProtocol,sourceURL)===null){
319342
sourceURL=pathToFileURL(sourceURL).href;
320343
}
321344
if(!SourceMap){
322345
SourceMap=require('internal/source_map/source_map').SourceMap;
323346
}
324-
letentry=esmSourceMapCache.get(sourceURL)??generatedSourceMapCache.get(sourceURL);
325-
if(entry===undefined){
326-
for(constvalueofgetCjsSourceMapCache()){
327-
constfilename=value.filename;
328-
constcachedSourceURL=value.sourceURL;
329-
if(sourceURL===filename||sourceURL===cachedSourceURL){
330-
entry=value;
331-
}
332-
}
333-
}
347+
constentry=getModuleSourceMapCache().get(sourceURL)??generatedSourceMapCache.get(sourceURL);
334348
if(entry===undefined){
335349
returnundefined;
336350
}

0 commit comments

Comments
 (0)