Skip to content

Commit b280d90

Browse files
joyeecheungaddaleax
authored andcommitted
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. Backport-PR-URL: #25964 PR-URL: #25352 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 508a2e7 commit b280d90

File tree

9 files changed

+199
-101
lines changed

9 files changed

+199
-101
lines changed

lib/internal/bootstrap/cache.js

+14-10
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.
@@ -69,11 +65,19 @@ if (!process.versions.openssl) {
6965
);
7066
}
7167

68+
const cachableBuiltins = [];
69+
for (const id of NativeModule.map.keys()) {
70+
if (id.startsWith('internal/deps') ||
71+
id.startsWith('v8/') || id.startsWith('node-inspect/')) {
72+
cannotUseCache.push(id);
73+
}
74+
if (!cannotUseCache.includes(id)) {
75+
cachableBuiltins.push(id);
76+
}
77+
}
78+
7279
module.exports = {
73-
cachableBuiltins: Object.keys(source).filter(
74-
(key) => !cannotUseCache.includes(key)
75-
),
76-
getSource(id) { return source[id]; },
80+
cachableBuiltins,
7781
getCodeCache,
7882
compileFunction,
7983
cannotUseCache

lib/internal/bootstrap/loaders.js

+42-70
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,62 +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-
// TODO(TimothyGu): remove when DEP0084 reaches end of life.
225-
NativeModule.isDepsModule(id)) {
226-
id = `internal/deps/${id}`;
215+
if (mod.loaded || mod.loading) {
216+
return mod.exports;
227217
}
228-
return NativeModule.require(id);
229-
};
230218

231-
NativeModule.getCached = function(id) {
232-
return NativeModule._cache[id];
219+
moduleLoadList.push(`NativeModule ${id}`);
220+
mod.compile();
221+
return mod.exports;
233222
};
234223

235224
NativeModule.exists = function(id) {
236-
return NativeModule._source.hasOwnProperty(id);
225+
return NativeModule.map.has(id);
237226
};
238227

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

262-
NativeModule.getSource = function(id) {
263-
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);
264240
};
265241

266242
const getOwn = (target, property, receiver) => {
@@ -334,13 +310,13 @@ NativeModule.prototype.compile = function() {
334310

335311
try {
336312
const requireFn = this.id.startsWith('internal/deps/') ?
337-
NativeModule.requireForDeps :
313+
NativeModule.requireWithFallbackInDeps :
338314
NativeModule.require;
339315

340316
const fn = compileFunction(id);
341317
fn(this.exports, requireFn, this, process, internalBinding);
342318

343-
if (config.experimentalModules && !NativeModule.isInternal(this.id)) {
319+
if (config.experimentalModules && this.canBeRequiredByUsers) {
344320
this.proxifyExports();
345321
}
346322

@@ -350,10 +326,6 @@ NativeModule.prototype.compile = function() {
350326
}
351327
};
352328

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

lib/internal/modules/cjs/loader.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,12 @@ function Module(id, parent) {
113113
this.children = [];
114114
}
115115

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

119123
Object.freeze(builtinModules);
120124
Module.builtinModules = builtinModules;
@@ -423,7 +427,7 @@ if (isWindows) {
423427
var indexChars = [ 105, 110, 100, 101, 120, 46 ];
424428
var indexLen = indexChars.length;
425429
Module._resolveLookupPaths = function(request, parent, newReturn) {
426-
if (NativeModule.nonInternalExists(request)) {
430+
if (NativeModule.canBeRequiredByUsers(request)) {
427431
debug('looking for %j in []', request);
428432
return (newReturn ? null : [request, []]);
429433
}
@@ -540,7 +544,7 @@ Module._load = function(request, parent, isMain) {
540544
return cachedModule.exports;
541545
}
542546

543-
if (NativeModule.nonInternalExists(filename)) {
547+
if (NativeModule.canBeRequiredByUsers(filename)) {
544548
debug('load native module %s', request);
545549
return NativeModule.require(filename);
546550
}
@@ -573,7 +577,7 @@ function tryModuleLoad(module, filename) {
573577
}
574578

575579
Module._resolveFilename = function(request, parent, isMain, options) {
576-
if (NativeModule.nonInternalExists(request)) {
580+
if (NativeModule.canBeRequiredByUsers(request)) {
577581
return request;
578582
}
579583

lib/internal/modules/esm/default_resolve.js

+1-1
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

+1-1
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

+15-5
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,21 @@ void NativeModuleLoader::GetCacheUsage(
8383
args.GetReturnValue().Set(result);
8484
}
8585

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

93103
void NativeModuleLoader::ConfigStringGetter(
@@ -306,8 +316,8 @@ void NativeModuleLoader::Initialize(Local<Object> target,
306316
.FromJust());
307317
CHECK(target
308318
->SetAccessor(env->context(),
309-
env->source_string(),
310-
SourceObjectGetter,
319+
FIXED_ONE_BYTE_STRING(env->isolate(), "moduleIds"),
320+
ModuleIdsGetter,
311321
nullptr,
312322
MaybeLocal<Value>(),
313323
DEFAULT,

src/node_native_module.h

+4-5
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ class NativeModuleLoader {
5858

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

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

+1-4
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)