Skip to content

Commit

Permalink
lib: convert WeakMaps in cjs loader with private symbol properties
Browse files Browse the repository at this point in the history
Symbol properties are typically more GC-efficient than using WeakMaps,
since WeakMap requires ephemeron GC. `module[kModuleExportNames]`
would be easier to read than `cjsParseCache.get(module).exportNames`
as well.
  • Loading branch information
legendecas committed Mar 18, 2024
1 parent 814fa1a commit 3f5c12a
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 39 deletions.
78 changes: 50 additions & 28 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const {
ReflectSet,
RegExpPrototypeExec,
SafeMap,
SafeWeakMap,
String,
StringPrototypeCharAt,
StringPrototypeCharCodeAt,
Expand All @@ -61,18 +60,41 @@ const {
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const {
privateSymbols: {
module_source_private_symbol,
module_export_names_private_symbol,
module_circular_visited_private_symbol,
module_export_private_symbol,
module_parent_private_symbol,
},
} = internalBinding('util');

// Map used to store CJS parsing data.
const cjsParseCache = new SafeWeakMap();
// Internal properties for Module instances.
/**
* Cached {@link Module} source string.
*/
const kModuleSource = module_source_private_symbol;
/**
* Cached {@link Module} export names for ESM loader.
*/
const kModuleExportNames = module_export_names_private_symbol;
/**
* {@link Module} circular dependency visited flag.
*/
const kModuleCircularVisited = module_circular_visited_private_symbol;
/**
* Map of already-loaded CJS modules to use.
* {@link Module} export object snapshot for ESM loader.
*/
const cjsExportsCache = new SafeWeakMap();
const kModuleExport = module_export_private_symbol;
const kModuleParent = module_parent_private_symbol;

// Set first due to cycle with ESM loader functions.
module.exports = {
cjsExportsCache,
cjsParseCache,
kModuleSource,
kModuleExport,
kModuleExportNames,
kModuleCircularVisited,
initializeCJS,
Module,
wrapSafe,
Expand Down Expand Up @@ -243,8 +265,6 @@ function reportModuleNotFoundToWatchMode(basePath, extensions) {
}
}

/** @type {Map<Module, Module>} */
const moduleParentCache = new SafeWeakMap();
/**
* Create a new module instance.
* @param {string} id
Expand All @@ -254,7 +274,7 @@ function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
setOwnProperty(this, 'exports', {});
moduleParentCache.set(this, parent);
this[kModuleParent] = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
Expand Down Expand Up @@ -342,17 +362,19 @@ ObjectDefineProperty(BuiltinModule.prototype, 'isPreloading', isPreloadingDesc);

/**
* Get the parent of the current module from our cache.
* @this {Module}
*/
function getModuleParent() {
return moduleParentCache.get(this);
return this[kModuleParent];
}

/**
* Set the parent of the current module in our cache.
* @this {Module}
* @param {Module} value
*/
function setModuleParent(value) {
moduleParentCache.set(this, value);
this[kModuleParent] = value;
}

let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
Expand Down Expand Up @@ -986,15 +1008,18 @@ Module._load = function(request, parent, isMain) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
if (!parseCachedModule || parseCachedModule.loaded) {
return getExportsForCircularRequire(cachedModule);
}
parseCachedModule.loaded = true;
} else {
if (cachedModule.loaded) {
return cachedModule.exports;
}
// If the cached module was finished loading, there are two possible conditions:
// 1. the cache entry was created by the ESM loader, or
// 2. it is circularly required.
// Return a proxy for the cached module's export object if the module is circularly required.
if (cachedModule[kModuleExportNames] === undefined || cachedModule[kModuleCircularVisited]) {
return getExportsForCircularRequire(cachedModule);
}
// This is an ESM loader created cache entry, mark it as visited and fallthrough to loading the module.
cachedModule[kModuleCircularVisited] = true;
}

if (BuiltinModule.canBeRequiredWithoutScheme(filename)) {
Expand Down Expand Up @@ -1132,7 +1157,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
const requireStack = [];
for (let cursor = parent;
cursor;
cursor = moduleParentCache.get(cursor)) {
cursor = cursor[kModuleParent]) {
ArrayPrototypePush(requireStack, cursor.filename || cursor.id);
}
let message = `Cannot find module '${request}'`;
Expand Down Expand Up @@ -1210,9 +1235,7 @@ Module.prototype.load = function(filename) {
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
// Preemptively cache for ESM loader.
if (!cjsExportsCache.has(this)) {
cjsExportsCache.set(this, exports);
}
this[kModuleExport] = exports;
};

/**
Expand Down Expand Up @@ -1368,11 +1391,10 @@ Module.prototype._compile = function(content, filename) {
*/
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const cached = cjsParseCache.get(module);
let content;
if (cached?.source) {
content = cached.source;
cached.source = undefined;
if (module[kModuleSource] !== undefined) {
content = module[kModuleSource];
module[kModuleSource] = undefined;
} else {
content = fs.readFileSync(filename, 'utf8');
}
Expand All @@ -1381,7 +1403,7 @@ Module._extensions['.js'] = function(module, filename) {
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = moduleParentCache.get(module);
const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const usesEsm = containsModuleSyntax(content, filename);
Expand Down
21 changes: 10 additions & 11 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ const {
} = require('internal/modules/helpers');
const {
Module: CJSModule,
cjsParseCache,
cjsExportsCache,
kModuleSource,
kModuleExport,
kModuleExportNames,
} = require('internal/modules/cjs/loader');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
Expand Down Expand Up @@ -298,9 +299,9 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
}

let exports;
if (cjsExportsCache.has(module)) {
exports = cjsExportsCache.get(module);
cjsExportsCache.delete(module);
if (module[kModuleExport] !== undefined) {
exports = module[kModuleExport];
module[kModuleExport] = undefined;
} else {
({ exports } = module);
}
Expand Down Expand Up @@ -370,11 +371,8 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
function cjsPreparseModuleExports(filename, source) {
// TODO: Do we want to keep hitting the user mutable CJS loader here?
let module = CJSModule._cache[filename];
if (module) {
const cached = cjsParseCache.get(module);
if (cached) {
return { module, exportNames: cached.exportNames };
}
if (module && module[kModuleExportNames] !== undefined) {
return { module, exportNames: module[kModuleExportNames] };
}
const loaded = Boolean(module);
if (!loaded) {
Expand All @@ -395,7 +393,8 @@ function cjsPreparseModuleExports(filename, source) {
const exportNames = new SafeSet(new SafeArrayIterator(exports));

// Set first for cycles.
cjsParseCache.set(module, { source, exportNames });
module[kModuleExportNames] = exportNames;
module[kModuleSource] = source;

if (reexports.length) {
module.filename = filename;
Expand Down
5 changes: 5 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
V(js_transferable_wrapper_private_symbol, "node:js_transferable_wrapper") \
V(entry_point_module_private_symbol, "node:entry_point_module") \
V(entry_point_promise_private_symbol, "node:entry_point_promise") \
V(module_source_private_symbol, "node:module_source") \
V(module_export_names_private_symbol, "node:module_export_names") \
V(module_circular_visited_private_symbol, "node:module_circular_visited") \
V(module_export_private_symbol, "node:module_export") \
V(module_parent_private_symbol, "node:module_parent") \
V(napi_type_tag, "node:napi:type_tag") \
V(napi_wrapper, "node:napi:wrapper") \
V(untransferable_object_private_symbol, "node:untransferableObject") \
Expand Down

0 comments on commit 3f5c12a

Please sign in to comment.