From c0c598d753d5ce7e1721215c6e6254dc02c023a8 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Tue, 28 May 2024 23:56:56 +0100 Subject: [PATCH] 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: https://github.com/nodejs/node/pull/51711 Reviewed-By: Joyee Cheung Reviewed-By: Antoine du Hamel --- lib/internal/modules/cjs/loader.js | 4 +- lib/internal/modules/esm/translators.js | 7 +- lib/internal/modules/esm/utils.js | 2 +- .../source_map/prepare_stack_trace.js | 25 ++- lib/internal/source_map/source_map_cache.js | 164 ++++++++++-------- .../source_map/source_map_cache_map.js | 115 ++++++++++++ lib/internal/util/iterable_weak_map.js | 84 --------- src/env_properties.h | 3 +- src/module_wrap.cc | 8 + test/fixtures/source-map/no-throw.js | 34 ++++ test/fixtures/source-map/no-throw.ts | 42 +++++ test/fixtures/source-map/no-throw2.js | 35 ++++ .../output/source_map_disabled_by_api.js | 4 +- .../output/source_map_prepare_stack_trace.js | 6 +- .../test-internal-iterable-weak-map.js | 101 ----------- .../test-source-map-cjs-require-cache.js | 36 ++++ 16 files changed, 393 insertions(+), 277 deletions(-) create mode 100644 lib/internal/source_map/source_map_cache_map.js delete mode 100644 lib/internal/util/iterable_weak_map.js create mode 100644 test/fixtures/source-map/no-throw.js create mode 100644 test/fixtures/source-map/no-throw.ts create mode 100644 test/fixtures/source-map/no-throw2.js delete mode 100644 test/parallel/test-internal-iterable-weak-map.js create mode 100644 test/parallel/test-source-map-cjs-require-cache.js diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 69041d154c6fa2..78274269d1a80c 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1335,7 +1335,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) { // Cache the source map for the module if present. const { sourceMapURL } = script; if (sourceMapURL) { - maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL); + maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, sourceMapURL); } return { @@ -1358,7 +1358,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) { // Cache the source map for the module if present. if (result.sourceMapURL) { - maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); + maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, result.sourceMapURL); } return result; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 89754b6453dbcc..bfd6a30d256c98 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -178,13 +178,12 @@ translators.set('module', function moduleStrategy(url, source, isMain) { function loadCJSModule(module, source, url, filename, isMain) { const compileResult = compileFunctionForCJSLoader(source, filename, isMain, false); + const { function: compiledWrapper, sourceMapURL } = compileResult; // Cache the source map for the cjs module if present. - if (compileResult.sourceMapURL) { - maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL); + if (sourceMapURL) { + maybeCacheSourceMap(url, source, module, false, undefined, sourceMapURL); } - const compiledWrapper = compileResult.function; - const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const __dirname = dirname(filename); // eslint-disable-next-line func-name-matching,func-style diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 70deb335f0e8c1..d393d4336a0c1e 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -341,7 +341,7 @@ function compileSourceTextModule(url, source, cascadedLoader) { } // Cache the source map for the module if present. if (wrap.sourceMapURL) { - maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL); + maybeCacheSourceMap(url, source, wrap, false, undefined, wrap.sourceMapURL); } return wrap; } diff --git a/lib/internal/source_map/prepare_stack_trace.js b/lib/internal/source_map/prepare_stack_trace.js index 776bb470bd13e4..1b12bb89f084ca 100644 --- a/lib/internal/source_map/prepare_stack_trace.js +++ b/lib/internal/source_map/prepare_stack_trace.js @@ -114,9 +114,15 @@ function getOriginalSymbolName(sourceMap, trace, curIndex) { } } -// Places a snippet of code from where the exception was originally thrown -// above the stack trace. This logic is modeled after GetErrorSource in -// node_errors.cc. +/** + * Return a snippet of code from where the exception was originally thrown + * above the stack trace. This called from GetErrorSource in node_errors.cc. + * @param {import('internal/source_map/source_map').SourceMap} sourceMap - the source map to be used + * @param {string} originalSourcePath - path or url of the original source + * @param {number} originalLine - line number in the original source + * @param {number} originalColumn - column number in the original source + * @returns {string | undefined} - the exact line in the source content or undefined if file not found + */ function getErrorSource( sourceMap, originalSourcePath, @@ -154,6 +160,12 @@ function getErrorSource( return exceptionLine; } +/** + * Retrieve the original source code from the source map's `sources` list or disk. + * @param {import('internal/source_map/source_map').SourceMap.payload} payload + * @param {string} originalSourcePath - path or url of the original source + * @returns {string | undefined} - the source content or undefined if file not found + */ function getOriginalSource(payload, originalSourcePath) { let source; // payload.sources has been normalized to be an array of absolute urls. @@ -177,6 +189,13 @@ function getOriginalSource(payload, originalSourcePath) { return source; } +/** + * Retrieve exact line in the original source code from the source map's `sources` list or disk. + * @param {string} fileName - actual file name + * @param {number} lineNumber - actual line number + * @param {number} columnNumber - actual column number + * @returns {string | undefined} - the source content or undefined if file not found + */ function getSourceMapErrorSource(fileName, lineNumber, columnNumber) { const sm = findSourceMap(fileName); if (sm === undefined) { diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js index 53c3374fc09176..f21b3719ad806a 100644 --- a/lib/internal/source_map/source_map_cache.js +++ b/lib/internal/source_map/source_map_cache.js @@ -3,7 +3,6 @@ const { ArrayPrototypePush, JSONParse, - ObjectKeys, RegExpPrototypeExec, SafeMap, StringPrototypeCodePointAt, @@ -25,17 +24,15 @@ const { } = require('internal/errors'); const { getLazy } = require('internal/util'); -// Since the CJS module cache is mutable, which leads to memory leaks when -// modules are deleted, we use a WeakMap so that the source map cache will -// be purged automatically: -const getCjsSourceMapCache = getLazy(() => { - const { IterableWeakMap } = require('internal/util/iterable_weak_map'); - return new IterableWeakMap(); +const getModuleSourceMapCache = getLazy(() => { + const { SourceMapCacheMap } = require('internal/source_map/source_map_cache_map'); + return new SourceMapCacheMap(); }); -// The esm cache is not mutable, so we can use a Map without memory concerns: -const esmSourceMapCache = new SafeMap(); -// The generated sources is not mutable, so we can use a Map without memory concerns: +// The generated source module/script instance is not accessible, so we can use +// a Map without memory concerns. Separate generated source entries with the module +// source entries to avoid overriding the module source entries with arbitrary +// source url magic comments. const generatedSourceMapCache = new SafeMap(); const kLeadingProtocol = /^\w+:\/\//; const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?[^\s]+)/g; @@ -52,6 +49,10 @@ function getSourceMapsEnabled() { return sourceMapsEnabled; } +/** + * Enables or disables source maps programmatically. + * @param {boolean} val + */ function setSourceMapsEnabled(val) { validateBoolean(val, 'val'); @@ -72,6 +73,14 @@ function setSourceMapsEnabled(val) { sourceMapsEnabled = val; } +/** + * Extracts the source url from the content if present. For example + * //# sourceURL=file:///path/to/file + * + * Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code + * @param {string} content - source content + * @returns {string | null} source url or null if not present + */ function extractSourceURLMagicComment(content) { let match; let matchSourceURL; @@ -90,6 +99,14 @@ function extractSourceURLMagicComment(content) { return sourceURL; } +/** + * Extracts the source map url from the content if present. For example + * //# sourceMappingURL=file:///path/to/file + * + * Read more at: https://tc39.es/source-map-spec/#linking-generated-code + * @param {string} content - source content + * @returns {string | null} source map url or null if not present + */ function extractSourceMapURLMagicComment(content) { let match; let lastMatch; @@ -104,7 +121,17 @@ function extractSourceMapURLMagicComment(content) { return lastMatch.groups.sourceMappingURL; } -function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) { +/** + * Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL. + * @param {string} filename - the actual filename + * @param {string} content - the actual source content + * @param {import('internal/modules/cjs/loader').Module | ModuleWrap} moduleInstance - a module instance that + * associated with the source, once this is reclaimed, the source map entry will be removed from the cache + * @param {boolean} isGeneratedSource - if the source was generated and evaluated with the global eval + * @param {string | undefined} sourceURL - the source url + * @param {string | undefined} sourceMapURL - the source map url + */ +function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSource, sourceURL, sourceMapURL) { const sourceMapsEnabled = getSourceMapsEnabled(); if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return; const { normalizeReferrerURL } = require('internal/modules/helpers'); @@ -130,45 +157,32 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo } const data = dataFromUrl(filename, sourceMapURL); - const url = data ? null : sourceMapURL; - if (cjsModuleInstance) { - getCjsSourceMapCache().set(cjsModuleInstance, { - __proto__: null, - filename, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }); - } else if (isGeneratedSource) { - const entry = { - __proto__: null, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }; + const entry = { + __proto__: null, + lineLengths: lineLengths(content), + data, + // Save the source map url if it is not a data url. + sourceMapURL: data ? null : sourceMapURL, + sourceURL, + }; + + if (isGeneratedSource) { 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. - const entry = { - __proto__: null, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }; - esmSourceMapCache.set(filename, entry); - if (sourceURL) { - esmSourceMapCache.set(sourceURL, entry); - } + return; } + // If it is not a generated source, we assume we are in a "cjs/esm" + // context. + const keys = sourceURL ? [filename, sourceURL] : [filename]; + getModuleSourceMapCache().set(keys, entry, moduleInstance); } +/** + * Caches the source map if it is present in the eval'd source. + * @param {string} content - the eval'd source code + */ function maybeCacheGeneratedSourceMap(content) { const sourceMapsEnabled = getSourceMapsEnabled(); if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return; @@ -186,6 +200,14 @@ function maybeCacheGeneratedSourceMap(content) { } } +/** + * Resolves source map payload data from the source url and source map url. + * If the source map url is a data url, the data is returned. + * Otherwise the source map url is resolved to a file path and the file is read. + * @param {string} sourceURL - url of the source file + * @param {string} sourceMappingURL - url of the source map + * @returns {object} deserialized source map JSON object + */ function dataFromUrl(sourceURL, sourceMappingURL) { try { const url = new URL(sourceMappingURL); @@ -227,7 +249,11 @@ function lineLengths(content) { return output; } - +/** + * Read source map from file. + * @param {string} mapURL - file url of the source map + * @returns {object} deserialized source map JSON object + */ function sourceMapFromFile(mapURL) { try { const fs = require('fs'); @@ -281,39 +307,36 @@ function sourcesToAbsolute(baseURL, data) { return data; } -// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during -// shutdown. In particular, they also run when Workers are terminated, making -// it important that they do not call out to any user-provided code, including -// built-in prototypes that might have been tampered with. +// WARNING: The `sourceMapCacheToObject` runs during shutdown. In particular, +// it also runs when Workers are terminated, making it important that it does +// not call out to any user-provided code, including built-in prototypes that +// might have been tampered with. // Get serialized representation of source-map cache, this is used // to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled. function sourceMapCacheToObject() { - const obj = { __proto__: null }; - - for (const { 0: k, 1: v } of esmSourceMapCache) { - obj[k] = v; - } - - appendCJSCache(obj); - - if (ObjectKeys(obj).length === 0) { + const moduleSourceMapCache = getModuleSourceMapCache(); + if (moduleSourceMapCache.size === 0) { return undefined; } - return obj; -} -function appendCJSCache(obj) { - for (const value of getCjsSourceMapCache()) { - obj[value.filename] = { + const obj = { __proto__: null }; + for (const { 0: k, 1: v } of moduleSourceMapCache) { + obj[k] = { __proto__: null, - lineLengths: value.lineLengths, - data: value.data, - url: value.url, + lineLengths: v.lineLengths, + data: v.data, + url: v.sourceMapURL, }; } + return obj; } +/** + * Find a source map for a given actual source URL or path. + * @param {string} sourceURL - actual source URL or path + * @returns {import('internal/source_map/source_map').SourceMap | undefined} a source map or undefined if not found + */ function findSourceMap(sourceURL) { if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) { sourceURL = pathToFileURL(sourceURL).href; @@ -321,16 +344,7 @@ function findSourceMap(sourceURL) { if (!SourceMap) { SourceMap = require('internal/source_map/source_map').SourceMap; } - let entry = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL); - if (entry === undefined) { - for (const value of getCjsSourceMapCache()) { - const filename = value.filename; - const cachedSourceURL = value.sourceURL; - if (sourceURL === filename || sourceURL === cachedSourceURL) { - entry = value; - } - } - } + const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL); if (entry === undefined) { return undefined; } diff --git a/lib/internal/source_map/source_map_cache_map.js b/lib/internal/source_map/source_map_cache_map.js new file mode 100644 index 00000000000000..e8adfe83708316 --- /dev/null +++ b/lib/internal/source_map/source_map_cache_map.js @@ -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>} + * 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, +}; diff --git a/lib/internal/util/iterable_weak_map.js b/lib/internal/util/iterable_weak_map.js deleted file mode 100644 index 16694ffdb11de8..00000000000000 --- a/lib/internal/util/iterable_weak_map.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -const { - ObjectFreeze, - SafeFinalizationRegistry, - SafeSet, - SafeWeakMap, - SafeWeakRef, - SymbolIterator, -} = primordials; - -// This class is modified from the example code in the WeakRefs specification: -// https://github.com/tc39/proposal-weakrefs -// Licensed under ECMA's MIT-style license, see: -// https://github.com/tc39/ecma262/blob/HEAD/LICENSE.md -class IterableWeakMap { - #weakMap = new SafeWeakMap(); - #refSet = new SafeSet(); - #finalizationGroup = new SafeFinalizationRegistry(cleanup); - - set(key, value) { - const entry = this.#weakMap.get(key); - if (entry) { - // If there's already an entry for the object represented by "key", - // the value can be updated without creating a new WeakRef: - this.#weakMap.set(key, { value, ref: entry.ref }); - } else { - const ref = new SafeWeakRef(key); - this.#weakMap.set(key, { value, ref }); - this.#refSet.add(ref); - this.#finalizationGroup.register(key, { - set: this.#refSet, - ref, - }, ref); - } - } - - get(key) { - return this.#weakMap.get(key)?.value; - } - - has(key) { - return this.#weakMap.has(key); - } - - delete(key) { - const entry = this.#weakMap.get(key); - if (!entry) { - return false; - } - this.#weakMap.delete(key); - this.#refSet.delete(entry.ref); - this.#finalizationGroup.unregister(entry.ref); - return true; - } - - [SymbolIterator]() { - const iterator = this.#refSet[SymbolIterator](); - - const next = () => { - const result = iterator.next(); - if (result.done) return result; - const key = result.value.deref(); - if (key == null) return next(); - const { value } = this.#weakMap.get(key); - return { done: false, value }; - }; - - return { - [SymbolIterator]() { return this; }, - next, - }; - } -} - -function cleanup({ set, ref }) { - set.delete(ref); -} - -ObjectFreeze(IterableWeakMap.prototype); - -module.exports = { - IterableWeakMap, -}; diff --git a/src/env_properties.h b/src/env_properties.h index 8bf62103a394d9..b8e0f8c3bb12cd 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -35,7 +35,8 @@ V(napi_wrapper, "node:napi:wrapper") \ V(untransferable_object_private_symbol, "node:untransferableObject") \ V(exit_info_private_symbol, "node:exit_info_private_symbol") \ - V(promise_trace_id, "node:promise_trace_id") + V(promise_trace_id, "node:promise_trace_id") \ + V(source_map_data_private_symbol, "node:source_map_data_private_symbol") // Symbols are per-isolate primitives but Environment proxies them // for the sake of convenience. diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 35139e874bca18..1e2ceb931bbfac 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -312,6 +312,14 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { return; } + // Initialize an empty slot for source map cache before the object is frozen. + if (that->SetPrivate(context, + realm->isolate_data()->source_map_data_private_symbol(), + Undefined(isolate)) + .IsNothing()) { + return; + } + // Use the extras object as an object whose GetCreationContext() will be the // original `context`, since the `Context` itself strictly speaking cannot // be stored in an internal field. diff --git a/test/fixtures/source-map/no-throw.js b/test/fixtures/source-map/no-throw.js new file mode 100644 index 00000000000000..4c4e5fa47158da --- /dev/null +++ b/test/fixtures/source-map/no-throw.js @@ -0,0 +1,34 @@ +var Foo = /** @class */ (function () { + function Foo(x) { + if (x === void 0) { x = 33; } + this.x = x ? x : 99; + if (this.x) { + this.methodA(); + } + else { + this.methodB(); + } + this.methodC(); + } + Foo.prototype.methodA = function () { + }; + Foo.prototype.methodB = function () { + }; + Foo.prototype.methodC = function () { + }; + Foo.prototype.methodD = function () { + }; + return Foo; +}()); +var a = new Foo(0); +var b = new Foo(33); +a.methodD(); +module.exports = { + a: a, + b: b, + Foo: Foo, +}; +// To recreate: +// +// npx tsc --outDir test/fixtures/source-map --sourceMap --inlineSources test/fixtures/source-map/no-throw.ts +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm8tdGhyb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJuby10aHJvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtJQUVFLGFBQWEsQ0FBTTtRQUFOLGtCQUFBLEVBQUEsTUFBTTtRQUNqQixJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDbkIsSUFBSSxJQUFJLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDWCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQzthQUFNLENBQUM7WUFDTixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQztRQUNELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUNoQixDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDRCxxQkFBTyxHQUFQO0lBRUEsQ0FBQztJQUNELHFCQUFPLEdBQVA7SUFFQSxDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDSCxVQUFDO0FBQUQsQ0FBQyxBQXZCRCxJQXVCQztBQUVELElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFBO0FBQ3BCLElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFBO0FBQ3JCLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtBQU1YLE1BQU0sQ0FBQyxPQUFPLEdBQUc7SUFDZixDQUFDLEdBQUE7SUFDRCxDQUFDLEdBQUE7SUFDRCxHQUFHLEtBQUE7Q0FDSixDQUFBO0FBRUQsZUFBZTtBQUNmLEVBQUU7QUFDRiw2R0FBNkciLCJzb3VyY2VzQ29udGVudCI6WyJjbGFzcyBGb28ge1xuICB4O1xuICBjb25zdHJ1Y3RvciAoeCA9IDMzKSB7XG4gICAgdGhpcy54ID0geCA/IHggOiA5OVxuICAgIGlmICh0aGlzLngpIHtcbiAgICAgIHRoaXMubWV0aG9kQSgpXG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMubWV0aG9kQigpXG4gICAgfVxuICAgIHRoaXMubWV0aG9kQygpXG4gIH1cbiAgbWV0aG9kQSAoKSB7XG5cbiAgfVxuICBtZXRob2RCICgpIHtcblxuICB9XG4gIG1ldGhvZEMgKCkge1xuXG4gIH1cbiAgbWV0aG9kRCAoKSB7XG5cbiAgfVxufVxuXG5jb25zdCBhID0gbmV3IEZvbygwKVxuY29uc3QgYiA9IG5ldyBGb28oMzMpXG5hLm1ldGhvZEQoKVxuXG5kZWNsYXJlIGNvbnN0IG1vZHVsZToge1xuICBleHBvcnRzOiBhbnlcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG4gIGEsXG4gIGIsXG4gIEZvbyxcbn1cblxuLy8gVG8gcmVjcmVhdGU6XG4vL1xuLy8gbnB4IHRzYyAtLW91dERpciB0ZXN0L2ZpeHR1cmVzL3NvdXJjZS1tYXAgLS1zb3VyY2VNYXAgLS1pbmxpbmVTb3VyY2VzIHRlc3QvZml4dHVyZXMvc291cmNlLW1hcC9uby10aHJvdy50c1xuIl19 \ No newline at end of file diff --git a/test/fixtures/source-map/no-throw.ts b/test/fixtures/source-map/no-throw.ts new file mode 100644 index 00000000000000..71d065bca933d2 --- /dev/null +++ b/test/fixtures/source-map/no-throw.ts @@ -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 diff --git a/test/fixtures/source-map/no-throw2.js b/test/fixtures/source-map/no-throw2.js new file mode 100644 index 00000000000000..57a294ff3a2a63 --- /dev/null +++ b/test/fixtures/source-map/no-throw2.js @@ -0,0 +1,35 @@ +var Foo = /** @class */ (function () { + function Foo(x) { + if (x === void 0) { x = 33; } + this.x = x ? x : 99; + if (this.x) { + this.methodA(); + } + else { + this.methodB(); + } + this.methodC(); + } + Foo.prototype.methodA = function () { + }; + Foo.prototype.methodB = function () { + }; + Foo.prototype.methodC = function () { + }; + Foo.prototype.methodD = function () { + }; + return Foo; +}()); +var a = new Foo(0); +var b = new Foo(33); +a.methodD(); +module.exports = { + a: a, + b: b, + Foo: Foo, +}; +// To recreate: +// +// npx tsc --outDir test/fixtures/source-map --sourceMap --inlineSources test/fixtures/source-map/no-throw.ts +// cp test/fixtures/source-map/no-throw.ts test/fixtures/source-map/no-throw2.ts +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm8tdGhyb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJuby10aHJvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtJQUVFLGFBQWEsQ0FBTTtRQUFOLGtCQUFBLEVBQUEsTUFBTTtRQUNqQixJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDbkIsSUFBSSxJQUFJLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDWCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQzthQUFNLENBQUM7WUFDTixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQztRQUNELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUNoQixDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDRCxxQkFBTyxHQUFQO0lBRUEsQ0FBQztJQUNELHFCQUFPLEdBQVA7SUFFQSxDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDSCxVQUFDO0FBQUQsQ0FBQyxBQXZCRCxJQXVCQztBQUVELElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFBO0FBQ3BCLElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFBO0FBQ3JCLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtBQU1YLE1BQU0sQ0FBQyxPQUFPLEdBQUc7SUFDZixDQUFDLEdBQUE7SUFDRCxDQUFDLEdBQUE7SUFDRCxHQUFHLEtBQUE7Q0FDSixDQUFBO0FBRUQsZUFBZTtBQUNmLEVBQUU7QUFDRiw2R0FBNkciLCJzb3VyY2VzQ29udGVudCI6WyJjbGFzcyBGb28ge1xuICB4O1xuICBjb25zdHJ1Y3RvciAoeCA9IDMzKSB7XG4gICAgdGhpcy54ID0geCA/IHggOiA5OVxuICAgIGlmICh0aGlzLngpIHtcbiAgICAgIHRoaXMubWV0aG9kQSgpXG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMubWV0aG9kQigpXG4gICAgfVxuICAgIHRoaXMubWV0aG9kQygpXG4gIH1cbiAgbWV0aG9kQSAoKSB7XG5cbiAgfVxuICBtZXRob2RCICgpIHtcblxuICB9XG4gIG1ldGhvZEMgKCkge1xuXG4gIH1cbiAgbWV0aG9kRCAoKSB7XG5cbiAgfVxufVxuXG5jb25zdCBhID0gbmV3IEZvbygwKVxuY29uc3QgYiA9IG5ldyBGb28oMzMpXG5hLm1ldGhvZEQoKVxuXG5kZWNsYXJlIGNvbnN0IG1vZHVsZToge1xuICBleHBvcnRzOiBhbnlcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG4gIGEsXG4gIGIsXG4gIEZvbyxcbn1cblxuLy8gVG8gcmVjcmVhdGU6XG4vL1xuLy8gbnB4IHRzYyAtLW91dERpciB0ZXN0L2ZpeHR1cmVzL3NvdXJjZS1tYXAgLS1zb3VyY2VNYXAgLS1pbmxpbmVTb3VyY2VzIHRlc3QvZml4dHVyZXMvc291cmNlLW1hcC9uby10aHJvdy50c1xuIl19 \ No newline at end of file diff --git a/test/fixtures/source-map/output/source_map_disabled_by_api.js b/test/fixtures/source-map/output/source_map_disabled_by_api.js index d94a6310cff7ae..8f455f26b6c9c4 100644 --- a/test/fixtures/source-map/output/source_map_disabled_by_api.js +++ b/test/fixtures/source-map/output/source_map_disabled_by_api.js @@ -15,10 +15,10 @@ try { console.log(e); } +// Delete the CJS module cache and loading the module again with source maps +// support enabled programmatically. delete require.cache[require .resolve('../enclosing-call-site-min.js')]; - -// Re-enable. process.setSourceMapsEnabled(true); assert.strictEqual(process.sourceMapsEnabled, true); diff --git a/test/fixtures/source-map/output/source_map_prepare_stack_trace.js b/test/fixtures/source-map/output/source_map_prepare_stack_trace.js index d49bad116b479f..1b04e0a3ac221b 100644 --- a/test/fixtures/source-map/output/source_map_prepare_stack_trace.js +++ b/test/fixtures/source-map/output/source_map_prepare_stack_trace.js @@ -20,10 +20,8 @@ try { console.log(e); } -delete require.cache[require - .resolve('../enclosing-call-site-min.js')]; - -// Disable +// Source maps support is disabled programmatically even without deleting the +// CJS module cache. process.setSourceMapsEnabled(false); assert.strictEqual(process.sourceMapsEnabled, false); diff --git a/test/parallel/test-internal-iterable-weak-map.js b/test/parallel/test-internal-iterable-weak-map.js deleted file mode 100644 index f2befe13da87f3..00000000000000 --- a/test/parallel/test-internal-iterable-weak-map.js +++ /dev/null @@ -1,101 +0,0 @@ -// Flags: --expose-gc --expose-internals -'use strict'; - -const common = require('../common'); -const { deepStrictEqual, strictEqual } = require('assert'); -const { IterableWeakMap } = require('internal/util/iterable_weak_map'); - -// Ensures iterating over the map does not rely on methods which can be -// mutated by users. -Reflect.getPrototypeOf(function*() {}).prototype.next = common.mustNotCall(); -Reflect.getPrototypeOf(new Set()[Symbol.iterator]()).next = - common.mustNotCall(); - -// It drops entry if a reference is no longer held. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - delete _cache.moduleB; - setImmediate(() => { - _cache; // eslint-disable-line no-unused-expressions - globalThis.gc(); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); - }); -} - -// It updates an existing entry, if the same key is provided twice. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'goodbye'); - wm.set(_cache.moduleB, 'goodnight'); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodnight']); -} - -// It allows entry to be deleted by key. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - wm.delete(_cache.moduleB); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); -} - -// It handles delete for key that does not exist. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleC, 'goodbye'); - wm.delete(_cache.moduleB); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); -} - -// It allows an entry to be fetched by key. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - strictEqual(wm.get(_cache.moduleB), 'discard'); -} - -// It returns true for has() if key exists. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - }; - wm.set(_cache.moduleA, 'hello'); - strictEqual(wm.has(_cache.moduleA), true); -} diff --git a/test/parallel/test-source-map-cjs-require-cache.js b/test/parallel/test-source-map-cjs-require-cache.js new file mode 100644 index 00000000000000..42ec0b229526fa --- /dev/null +++ b/test/parallel/test-source-map-cjs-require-cache.js @@ -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; +});