Skip to content

Commit 92e95f1

Browse files
committed
src: simplify NativeModule caching and remove redundant data
- Remove `NativeModule._source` - the compilation is now entirely done in C++ and `process.binding('natives')` is implemented directly in the binding loader so there is no need to store additional source code strings. - Instead of using an object as `NativeModule._cached` and insert into it after compilation of each native module, simply prebuild a JS map filled with all the native modules and infer the state of compilation through `mod.loading`/`mod.loaded`. - Rename `NativeModule.nonInternalExists` to `NativeModule.canBeRequiredByUsers` and precompute that property for all the native modules during bootstrap instead of branching in every require call during runtime. This also fixes the bug where `worker_threads` can be made available with `--expose-internals`. - Rename `NativeModule.requireForDeps` to `NativeModule.requireWithFallbackInDeps`. - Add a test to make sure we do not accidentally leak any module to the global namespace. PR-URL: #25352 Reviewed-By: Anna Henningsen <[email protected]>
1 parent 18d3aeb commit 92e95f1

File tree

9 files changed

+198
-99
lines changed

9 files changed

+198
-99
lines changed

lib/internal/bootstrap/cache.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,10 @@
77

88
const { NativeModule } = require('internal/bootstrap/loaders');
99
const {
10-
source, getCodeCache, compileFunction
10+
getCodeCache, compileFunction
1111
} = internalBinding('native_module');
1212
const { hasTracing, hasInspector } = process.binding('config');
1313

14-
const depsModule = Object.keys(source).filter(
15-
(key) => NativeModule.isDepsModule(key) || key.startsWith('internal/deps')
16-
);
17-
1814
// Modules with source code compiled in js2c that
1915
// cannot be compiled with the code cache.
2016
const cannotUseCache = [
@@ -29,7 +25,7 @@ const cannotUseCache = [
2925
// the code cache is also used when compiling these two files.
3026
'internal/bootstrap/loaders',
3127
'internal/bootstrap/node'
32-
].concat(depsModule);
28+
];
3329

3430
// Skip modules that cannot be required when they are not
3531
// built into the binary.
@@ -67,11 +63,18 @@ if (!process.versions.openssl) {
6763
);
6864
}
6965

66+
const cachableBuiltins = [];
67+
for (const id of NativeModule.map.keys()) {
68+
if (id.startsWith('internal/deps')) {
69+
cannotUseCache.push(id);
70+
}
71+
if (!cannotUseCache.includes(id)) {
72+
cachableBuiltins.push(id);
73+
}
74+
}
75+
7076
module.exports = {
71-
cachableBuiltins: Object.keys(source).filter(
72-
(key) => !cannotUseCache.includes(key)
73-
),
74-
getSource(id) { return source[id]; },
77+
cachableBuiltins,
7578
getCodeCache,
7679
compileFunction,
7780
cannotUseCache

lib/internal/bootstrap/loaders.js

Lines changed: 42 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ let internalBinding;
158158
// Create this WeakMap in js-land because V8 has no C++ API for WeakMap.
159159
internalBinding('module_wrap').callbackMap = new WeakMap();
160160

161+
// Think of this as module.exports in this file even though it is not
162+
// written in CommonJS style.
163+
const loaderExports = { internalBinding, NativeModule };
164+
const loaderId = 'internal/bootstrap/loaders';
165+
const config = internalBinding('config');
166+
161167
// Set up NativeModule.
162168
function NativeModule(id) {
163169
this.filename = `${id}.js`;
@@ -167,34 +173,35 @@ function NativeModule(id) {
167173
this.exportKeys = undefined;
168174
this.loaded = false;
169175
this.loading = false;
176+
if (id === loaderId) {
177+
// Do not expose this to user land even with --expose-internals.
178+
this.canBeRequiredByUsers = false;
179+
} else if (id.startsWith('internal/')) {
180+
this.canBeRequiredByUsers = config.exposeInternals;
181+
} else {
182+
this.canBeRequiredByUsers = true;
183+
}
170184
}
171185

172186
const {
173-
source,
187+
moduleIds,
174188
compileFunction
175189
} = internalBinding('native_module');
176190

177-
NativeModule._source = source;
178-
NativeModule._cache = {};
179-
180-
const config = internalBinding('config');
181-
182-
// Think of this as module.exports in this file even though it is not
183-
// written in CommonJS style.
184-
const loaderExports = { internalBinding, NativeModule };
185-
const loaderId = 'internal/bootstrap/loaders';
191+
NativeModule.map = new Map();
192+
for (var i = 0; i < moduleIds.length; ++i) {
193+
const id = moduleIds[i];
194+
const mod = new NativeModule(id);
195+
NativeModule.map.set(id, mod);
196+
}
186197

187198
NativeModule.require = function(id) {
188199
if (id === loaderId) {
189200
return loaderExports;
190201
}
191202

192-
const cached = NativeModule.getCached(id);
193-
if (cached && (cached.loaded || cached.loading)) {
194-
return cached.exports;
195-
}
196-
197-
if (!NativeModule.exists(id)) {
203+
const mod = NativeModule.map.get(id);
204+
if (!mod) {
198205
// Model the error off the internal/errors.js model, but
199206
// do not use that module given that it could actually be
200207
// the one causing the error if there's a bug in Node.js.
@@ -205,60 +212,31 @@ NativeModule.require = function(id) {
205212
throw err;
206213
}
207214

208-
moduleLoadList.push(`NativeModule ${id}`);
209-
210-
const nativeModule = new NativeModule(id);
211-
212-
nativeModule.cache();
213-
nativeModule.compile();
214-
215-
return nativeModule.exports;
216-
};
217-
218-
NativeModule.isDepsModule = function(id) {
219-
return id.startsWith('node-inspect/') || id.startsWith('v8/');
220-
};
221-
222-
NativeModule.requireForDeps = function(id) {
223-
if (!NativeModule.exists(id)) {
224-
id = `internal/deps/${id}`;
215+
if (mod.loaded || mod.loading) {
216+
return mod.exports;
225217
}
226-
return NativeModule.require(id);
227-
};
228218

229-
NativeModule.getCached = function(id) {
230-
return NativeModule._cache[id];
219+
moduleLoadList.push(`NativeModule ${id}`);
220+
mod.compile();
221+
return mod.exports;
231222
};
232223

233224
NativeModule.exists = function(id) {
234-
return NativeModule._source.hasOwnProperty(id);
225+
return NativeModule.map.has(id);
235226
};
236227

237-
if (config.exposeInternals) {
238-
NativeModule.nonInternalExists = function(id) {
239-
// Do not expose this to user land even with --expose-internals.
240-
if (id === loaderId) {
241-
return false;
242-
}
243-
return NativeModule.exists(id);
244-
};
245-
246-
NativeModule.isInternal = function(id) {
247-
// Do not expose this to user land even with --expose-internals.
248-
return id === loaderId;
249-
};
250-
} else {
251-
NativeModule.nonInternalExists = function(id) {
252-
return NativeModule.exists(id) && !NativeModule.isInternal(id);
253-
};
254-
255-
NativeModule.isInternal = function(id) {
256-
return id.startsWith('internal/');
257-
};
258-
}
228+
NativeModule.canBeRequiredByUsers = function(id) {
229+
const mod = NativeModule.map.get(id);
230+
return mod && mod.canBeRequiredByUsers;
231+
};
259232

260-
NativeModule.getSource = function(id) {
261-
return NativeModule._source[id];
233+
// Allow internal modules from dependencies to require
234+
// other modules from dependencies by providing fallbacks.
235+
NativeModule.requireWithFallbackInDeps = function(request) {
236+
if (!NativeModule.map.has(request)) {
237+
request = `internal/deps/${request}`;
238+
}
239+
return NativeModule.require(request);
262240
};
263241

264242
const getOwn = (target, property, receiver) => {
@@ -332,13 +310,13 @@ NativeModule.prototype.compile = function() {
332310

333311
try {
334312
const requireFn = this.id.startsWith('internal/deps/') ?
335-
NativeModule.requireForDeps :
313+
NativeModule.requireWithFallbackInDeps :
336314
NativeModule.require;
337315

338316
const fn = compileFunction(id);
339317
fn(this.exports, requireFn, this, process, internalBinding);
340318

341-
if (config.experimentalModules && !NativeModule.isInternal(this.id)) {
319+
if (config.experimentalModules && this.canBeRequiredByUsers) {
342320
this.proxifyExports();
343321
}
344322

@@ -348,10 +326,6 @@ NativeModule.prototype.compile = function() {
348326
}
349327
};
350328

351-
NativeModule.prototype.cache = function() {
352-
NativeModule._cache[this.id] = this;
353-
};
354-
355329
// Coverage must be turned on early, so that we can collect
356330
// it for Node.js' own internal libraries.
357331
if (process.env.NODE_V8_COVERAGE) {

lib/internal/modules/cjs/loader.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,12 @@ function Module(id, parent) {
112112
this.children = [];
113113
}
114114

115-
const builtinModules = Object.keys(NativeModule._source)
116-
.filter(NativeModule.nonInternalExists);
115+
const builtinModules = [];
116+
for (const [id, mod] of NativeModule.map) {
117+
if (mod.canBeRequiredByUsers) {
118+
builtinModules.push(id);
119+
}
120+
}
117121

118122
Object.freeze(builtinModules);
119123
Module.builtinModules = builtinModules;
@@ -417,7 +421,7 @@ if (isWindows) {
417421
var indexChars = [ 105, 110, 100, 101, 120, 46 ];
418422
var indexLen = indexChars.length;
419423
Module._resolveLookupPaths = function(request, parent, newReturn) {
420-
if (NativeModule.nonInternalExists(request)) {
424+
if (NativeModule.canBeRequiredByUsers(request)) {
421425
debug('looking for %j in []', request);
422426
return (newReturn ? null : [request, []]);
423427
}
@@ -534,7 +538,7 @@ Module._load = function(request, parent, isMain) {
534538
return cachedModule.exports;
535539
}
536540

537-
if (NativeModule.nonInternalExists(filename)) {
541+
if (NativeModule.canBeRequiredByUsers(filename)) {
538542
debug('load native module %s', request);
539543
return NativeModule.require(filename);
540544
}
@@ -567,7 +571,7 @@ function tryModuleLoad(module, filename) {
567571
}
568572

569573
Module._resolveFilename = function(request, parent, isMain, options) {
570-
if (NativeModule.nonInternalExists(request)) {
574+
if (NativeModule.canBeRequiredByUsers(request)) {
571575
return request;
572576
}
573577

lib/internal/modules/esm/default_resolve.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const extensionFormatMap = {
5353
};
5454

5555
function resolve(specifier, parentURL) {
56-
if (NativeModule.nonInternalExists(specifier)) {
56+
if (NativeModule.canBeRequiredByUsers(specifier)) {
5757
return {
5858
url: specifier,
5959
format: 'builtin'

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ translators.set('builtin', async (url) => {
8181
// slice 'node:' scheme
8282
const id = url.slice(5);
8383
NativeModule.require(id);
84-
const module = NativeModule.getCached(id);
84+
const module = NativeModule.map.get(id);
8585
return createDynamicModule(
8686
[...module.exportKeys, 'default'], url, (reflect) => {
8787
debug(`Loading BuiltinModule ${url}`);

src/node_native_module.cc

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,21 @@ void NativeModuleLoader::GetCacheUsage(
8080
args.GetReturnValue().Set(result);
8181
}
8282

83-
void NativeModuleLoader::SourceObjectGetter(
83+
void NativeModuleLoader::ModuleIdsGetter(
8484
Local<Name> property, const PropertyCallbackInfo<Value>& info) {
8585
Local<Context> context = info.GetIsolate()->GetCurrentContext();
86-
info.GetReturnValue().Set(
87-
per_process::native_module_loader.GetSourceObject(context));
86+
Isolate* isolate = info.GetIsolate();
87+
88+
const NativeModuleRecordMap& source_ =
89+
per_process::native_module_loader.source_;
90+
std::vector<Local<Value>> ids;
91+
ids.reserve(source_.size());
92+
93+
for (auto const& x : source_) {
94+
ids.push_back(OneByteString(isolate, x.first.c_str(), x.first.size()));
95+
}
96+
97+
info.GetReturnValue().Set(Array::New(isolate, ids.data(), ids.size()));
8898
}
8999

90100
void NativeModuleLoader::ConfigStringGetter(
@@ -303,8 +313,8 @@ void NativeModuleLoader::Initialize(Local<Object> target,
303313
.FromJust());
304314
CHECK(target
305315
->SetAccessor(env->context(),
306-
env->source_string(),
307-
SourceObjectGetter,
316+
FIXED_ONE_BYTE_STRING(env->isolate(), "moduleIds"),
317+
ModuleIdsGetter,
308318
nullptr,
309319
MaybeLocal<Value>(),
310320
DEFAULT,

src/node_native_module.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@ class NativeModuleLoader {
5656

5757
private:
5858
static void GetCacheUsage(const v8::FunctionCallbackInfo<v8::Value>& args);
59-
// Passing map of builtin module source code into JS land as
60-
// internalBinding('native_module').source
61-
static void SourceObjectGetter(
62-
v8::Local<v8::Name> property,
63-
const v8::PropertyCallbackInfo<v8::Value>& info);
59+
// Passing ids of builtin module source code into JS land as
60+
// internalBinding('native_module').moduleIds
61+
static void ModuleIdsGetter(v8::Local<v8::Name> property,
62+
const v8::PropertyCallbackInfo<v8::Value>& info);
6463
// Passing config.gypi into JS land as internalBinding('native_module').config
6564
static void ConfigStringGetter(
6665
v8::Local<v8::Name> property,

test/code-cache/test-code-cache.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
// and the cache is used when built in modules are compiled.
66
// Otherwise, verifies that no cache is used when compiling builtins.
77

8-
require('../common');
8+
const { isMainThread } = require('../common');
99
const assert = require('assert');
1010
const {
1111
cachableBuiltins,
1212
cannotUseCache
1313
} = require('internal/bootstrap/cache');
14-
const {
15-
isMainThread
16-
} = require('worker_threads');
1714

1815
const {
1916
internalBinding

0 commit comments

Comments
 (0)