-
Notifications
You must be signed in to change notification settings - Fork 30.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
16 changed files
with
393 additions
and
277 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
'use strict'; | ||
|
||
const { | ||
ArrayPrototypeForEach, | ||
ObjectFreeze, | ||
SafeFinalizationRegistry, | ||
SafeMap, | ||
SafeWeakRef, | ||
SymbolIterator, | ||
} = primordials; | ||
const { | ||
privateSymbols: { | ||
source_map_data_private_symbol, | ||
}, | ||
} = internalBinding('util'); | ||
|
||
/** | ||
* Specialized map of WeakRefs to module instances that caches source map | ||
* entries by `filename` and `sourceURL`. Cached entries can be iterated with | ||
* `for..of` syntax. | ||
* | ||
* The cache map maintains the cache entries by: | ||
* - `weakModuleMap`(Map): a strong sourceURL -> WeakRef(Module), | ||
* - WeakRef(Module[source_map_data_private_symbol]): source map data. | ||
* | ||
* Obsolete `weakModuleMap` entries are removed by the `finalizationRegistry` | ||
* callback. This pattern decouples the strong url reference to the source map | ||
* data and allow the cache to be reclaimed eagerly, without depending on an | ||
* undeterministic callback of a finalization registry. | ||
*/ | ||
class SourceMapCacheMap { | ||
/** | ||
* @type {Map<string, WeakRef<*>>} | ||
* The cached module instance can be removed from the global module registry | ||
* with approaches like mutating `require.cache`. | ||
* The `weakModuleMap` exposes entries by `filename` and `sourceURL`. | ||
* In the case of mutated module registry, obsolete entries are removed from | ||
* the cache by the `finalizationRegistry`. | ||
*/ | ||
#weakModuleMap = new SafeMap(); | ||
|
||
#cleanup = ({ keys }) => { | ||
// Delete the entry if the weak target has been reclaimed. | ||
// If the weak target is not reclaimed, the entry was overridden by a new | ||
// weak target. | ||
ArrayPrototypeForEach(keys, (key) => { | ||
const ref = this.#weakModuleMap.get(key); | ||
if (ref && ref.deref() === undefined) { | ||
this.#weakModuleMap.delete(key); | ||
} | ||
}); | ||
}; | ||
#finalizationRegistry = new SafeFinalizationRegistry(this.#cleanup); | ||
|
||
/** | ||
* Sets the value for the given key, associated with the given module | ||
* instance. | ||
* @param {string[]} keys array of urls to index the value entry. | ||
* @param {*} sourceMapData the value entry. | ||
* @param {object} moduleInstance an object that can be weakly referenced and | ||
* invalidate the [key, value] entry after this object is reclaimed. | ||
*/ | ||
set(keys, sourceMapData, moduleInstance) { | ||
const weakRef = new SafeWeakRef(moduleInstance); | ||
ArrayPrototypeForEach(keys, (key) => this.#weakModuleMap.set(key, weakRef)); | ||
moduleInstance[source_map_data_private_symbol] = sourceMapData; | ||
this.#finalizationRegistry.register(moduleInstance, { keys }); | ||
} | ||
|
||
/** | ||
* Get an entry by the given key. | ||
* @param {string} key a file url or source url | ||
*/ | ||
get(key) { | ||
const weakRef = this.#weakModuleMap.get(key); | ||
const moduleInstance = weakRef?.deref(); | ||
if (moduleInstance === undefined) { | ||
return; | ||
} | ||
return moduleInstance[source_map_data_private_symbol]; | ||
} | ||
|
||
/** | ||
* Estimate the size of the cache. The actual size may be smaller because | ||
* some entries may be reclaimed with the module instance. | ||
*/ | ||
get size() { | ||
return this.#weakModuleMap.size; | ||
} | ||
|
||
[SymbolIterator]() { | ||
const iterator = this.#weakModuleMap.entries(); | ||
|
||
const next = () => { | ||
const result = iterator.next(); | ||
if (result.done) return result; | ||
const { 0: key, 1: weakRef } = result.value; | ||
const moduleInstance = weakRef.deref(); | ||
if (moduleInstance == null) return next(); | ||
const value = moduleInstance[source_map_data_private_symbol]; | ||
return { done: false, value: [key, value] }; | ||
}; | ||
|
||
return { | ||
[SymbolIterator]() { return this; }, | ||
next, | ||
}; | ||
} | ||
} | ||
|
||
ObjectFreeze(SourceMapCacheMap.prototype); | ||
|
||
module.exports = { | ||
SourceMapCacheMap, | ||
}; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
class Foo { | ||
x; | ||
constructor (x = 33) { | ||
this.x = x ? x : 99 | ||
if (this.x) { | ||
this.methodA() | ||
} else { | ||
this.methodB() | ||
} | ||
this.methodC() | ||
} | ||
methodA () { | ||
|
||
} | ||
methodB () { | ||
|
||
} | ||
methodC () { | ||
|
||
} | ||
methodD () { | ||
|
||
} | ||
} | ||
|
||
const a = new Foo(0) | ||
const b = new Foo(33) | ||
a.methodD() | ||
|
||
declare const module: { | ||
exports: any | ||
} | ||
|
||
module.exports = { | ||
a, | ||
b, | ||
Foo, | ||
} | ||
|
||
// To recreate: | ||
// | ||
// npx tsc --outDir test/fixtures/source-map --inlineSourceMap --inlineSources test/fixtures/source-map/no-throw.ts |
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Flags: --enable-source-maps --max-old-space-size=10 --expose-gc | ||
|
||
/** | ||
* This test verifies that the source map of a CJS module is cleared after the | ||
* CJS module is reclaimed by GC. | ||
*/ | ||
|
||
'use strict'; | ||
const common = require('../common'); | ||
const assert = require('node:assert'); | ||
const { findSourceMap } = require('node:module'); | ||
|
||
const moduleId = require.resolve('../fixtures/source-map/no-throw.js'); | ||
const moduleIdRepeat = require.resolve('../fixtures/source-map/no-throw2.js'); | ||
|
||
function run(moduleId) { | ||
require(moduleId); | ||
delete require.cache[moduleId]; | ||
const idx = module.children.findIndex((child) => child.id === moduleId); | ||
assert.ok(idx >= 0); | ||
module.children.splice(idx, 1); | ||
|
||
// Verify that the source map is still available | ||
assert.notStrictEqual(findSourceMap(moduleId), undefined); | ||
} | ||
|
||
// Run the test in a function scope so that every variable can be reclaimed by GC. | ||
run(moduleId); | ||
|
||
// Run until the source map is cleared by GC, or fail the test after determined iterations. | ||
common.gcUntil('SourceMap of deleted CJS module is cleared', () => { | ||
// Repetitively load a second module with --max-old-space-size=10 to make GC more aggressive. | ||
run(moduleIdRepeat); | ||
// Verify that the source map is cleared. | ||
return findSourceMap(moduleId) == null; | ||
}); |