Skip to content

Commit a412115

Browse files
legendecasbmeck
authored andcommitted
lib,src: iterate module requests of a module wrap in JS
Avoid repetitively calling into JS callback from C++ in `ModuleWrap::Link`. This removes the convoluted callback style of the internal `ModuleWrap` link step. PR-URL: nodejs#52058 Reviewed-By: Joyee Cheung <[email protected]>
1 parent 3989d13 commit a412115

File tree

7 files changed

+199
-229
lines changed

7 files changed

+199
-229
lines changed

lib/internal/modules/esm/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ class ModuleLoader {
319319
* @param {object} importAttributes import attributes from the import statement.
320320
* @returns {ModuleJobBase}
321321
*/
322-
getModuleWrapForRequire(specifier, parentURL, importAttributes) {
322+
getModuleJobForRequire(specifier, parentURL, importAttributes) {
323323
assert(getOptionValue('--experimental-require-module'));
324324

325325
if (canParse(specifier)) {

lib/internal/modules/esm/module_job.js

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
22

33
const {
4+
Array,
45
ArrayPrototypeJoin,
5-
ArrayPrototypePush,
66
ArrayPrototypeSome,
77
FunctionPrototype,
88
ObjectSetPrototypeOf,
@@ -87,31 +87,8 @@ class ModuleJob extends ModuleJobBase {
8787
this.modulePromise = PromiseResolve(this.modulePromise);
8888
}
8989

90-
// Wait for the ModuleWrap instance being linked with all dependencies.
91-
const link = async () => {
92-
this.module = await this.modulePromise;
93-
assert(this.module instanceof ModuleWrap);
94-
95-
// Explicitly keeping track of dependency jobs is needed in order
96-
// to flatten out the dependency graph below in `_instantiate()`,
97-
// so that circular dependencies can't cause a deadlock by two of
98-
// these `link` callbacks depending on each other.
99-
const dependencyJobs = [];
100-
const promises = this.module.link(async (specifier, attributes) => {
101-
const job = await this.#loader.getModuleJob(specifier, url, attributes);
102-
debug(`async link() ${this.url} -> ${specifier}`, job);
103-
ArrayPrototypePush(dependencyJobs, job);
104-
return job.modulePromise;
105-
});
106-
107-
if (promises !== undefined) {
108-
await SafePromiseAllReturnVoid(promises);
109-
}
110-
111-
return SafePromiseAllReturnArrayLike(dependencyJobs);
112-
};
11390
// Promise for the list of all dependencyJobs.
114-
this.linked = link();
91+
this.linked = this._link();
11592
// This promise is awaited later anyway, so silence
11693
// 'unhandled rejection' warnings.
11794
PromisePrototypeThen(this.linked, undefined, noop);
@@ -121,6 +98,49 @@ class ModuleJob extends ModuleJobBase {
12198
this.instantiated = undefined;
12299
}
123100

101+
/**
102+
* Iterates the module requests and links with the loader.
103+
* @returns {Promise<ModuleJob[]>} Dependency module jobs.
104+
*/
105+
async _link() {
106+
this.module = await this.modulePromise;
107+
assert(this.module instanceof ModuleWrap);
108+
109+
const moduleRequests = this.module.getModuleRequests();
110+
// Explicitly keeping track of dependency jobs is needed in order
111+
// to flatten out the dependency graph below in `_instantiate()`,
112+
// so that circular dependencies can't cause a deadlock by two of
113+
// these `link` callbacks depending on each other.
114+
// Create an ArrayLike to avoid calling into userspace with `.then`
115+
// when returned from the async function.
116+
const dependencyJobs = Array(moduleRequests.length);
117+
ObjectSetPrototypeOf(dependencyJobs, null);
118+
119+
// Specifiers should be aligned with the moduleRequests array in order.
120+
const specifiers = Array(moduleRequests.length);
121+
const modulePromises = Array(moduleRequests.length);
122+
// Iterate with index to avoid calling into userspace with `Symbol.iterator`.
123+
for (let idx = 0; idx < moduleRequests.length; idx++) {
124+
const { specifier, attributes } = moduleRequests[idx];
125+
126+
const dependencyJobPromise = this.#loader.getModuleJob(
127+
specifier, this.url, attributes,
128+
);
129+
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
130+
debug(`async link() ${this.url} -> ${specifier}`, job);
131+
dependencyJobs[idx] = job;
132+
return job.modulePromise;
133+
});
134+
modulePromises[idx] = modulePromise;
135+
specifiers[idx] = specifier;
136+
}
137+
138+
const modules = await SafePromiseAllReturnArrayLike(modulePromises);
139+
this.module.link(specifiers, modules);
140+
141+
return dependencyJobs;
142+
}
143+
124144
instantiate() {
125145
if (this.instantiated === undefined) {
126146
this.instantiated = this._instantiate();
@@ -277,18 +297,20 @@ class ModuleJobSync extends ModuleJobBase {
277297
super(url, importAttributes, moduleWrap, isMain, inspectBrk, true);
278298
assert(this.module instanceof ModuleWrap);
279299
this.#loader = loader;
280-
const moduleRequests = this.module.getModuleRequestsSync();
281-
const linked = [];
300+
const moduleRequests = this.module.getModuleRequests();
301+
// Specifiers should be aligned with the moduleRequests array in order.
302+
const specifiers = Array(moduleRequests.length);
303+
const modules = Array(moduleRequests.length);
304+
const jobs = Array(moduleRequests.length);
282305
for (let i = 0; i < moduleRequests.length; ++i) {
283-
const { 0: specifier, 1: attributes } = moduleRequests[i];
284-
const job = this.#loader.getModuleWrapForRequire(specifier, url, attributes);
285-
const isLast = (i === moduleRequests.length - 1);
286-
// TODO(joyeecheung): make the resolution callback deal with both promisified
287-
// an raw module wraps, then we don't need to wrap it with a promise here.
288-
this.module.cacheResolvedWrapsSync(specifier, PromiseResolve(job.module), isLast);
289-
ArrayPrototypePush(linked, job);
306+
const { specifier, attributes } = moduleRequests[i];
307+
const job = this.#loader.getModuleJobForRequire(specifier, url, attributes);
308+
specifiers[i] = specifier;
309+
modules[i] = job.module;
310+
jobs[i] = job;
290311
}
291-
this.linked = linked;
312+
this.module.link(specifiers, modules);
313+
this.linked = jobs;
292314
}
293315

294316
get modulePromise() {

lib/internal/vm/module.js

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
'use strict';
22

33
const {
4+
Array,
45
ArrayIsArray,
56
ArrayPrototypeForEach,
67
ArrayPrototypeIndexOf,
8+
ArrayPrototypeMap,
79
ArrayPrototypeSome,
810
ObjectDefineProperty,
911
ObjectFreeze,
1012
ObjectGetPrototypeOf,
1113
ObjectPrototypeHasOwnProperty,
1214
ObjectSetPrototypeOf,
15+
PromiseResolve,
16+
PromisePrototypeThen,
1317
ReflectApply,
14-
SafePromiseAllReturnVoid,
18+
SafePromiseAllReturnArrayLike,
1519
Symbol,
1620
SymbolToStringTag,
1721
TypeError,
@@ -294,44 +298,62 @@ class SourceTextModule extends Module {
294298
importModuleDynamically,
295299
});
296300

297-
this[kLink] = async (linker) => {
298-
this.#statusOverride = 'linking';
301+
this[kDependencySpecifiers] = undefined;
302+
}
299303

300-
const promises = this[kWrap].link(async (identifier, attributes) => {
301-
const module = await linker(identifier, this, { attributes, assert: attributes });
302-
if (!isModule(module)) {
303-
throw new ERR_VM_MODULE_NOT_MODULE();
304-
}
305-
if (module.context !== this.context) {
306-
throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
307-
}
308-
if (module.status === 'errored') {
309-
throw new ERR_VM_MODULE_LINK_FAILURE(`request for '${identifier}' resolved to an errored module`, module.error);
310-
}
311-
if (module.status === 'unlinked') {
312-
await module[kLink](linker);
313-
}
314-
return module[kWrap];
304+
async [kLink](linker) {
305+
this.#statusOverride = 'linking';
306+
307+
const moduleRequests = this[kWrap].getModuleRequests();
308+
// Iterates the module requests and links with the linker.
309+
// Specifiers should be aligned with the moduleRequests array in order.
310+
const specifiers = Array(moduleRequests.length);
311+
const modulePromises = Array(moduleRequests.length);
312+
// Iterates with index to avoid calling into userspace with `Symbol.iterator`.
313+
for (let idx = 0; idx < moduleRequests.length; idx++) {
314+
const { specifier, attributes } = moduleRequests[idx];
315+
316+
const linkerResult = linker(specifier, this, {
317+
attributes,
318+
assert: attributes,
315319
});
320+
const modulePromise = PromisePrototypeThen(
321+
PromiseResolve(linkerResult), async (module) => {
322+
if (!isModule(module)) {
323+
throw new ERR_VM_MODULE_NOT_MODULE();
324+
}
325+
if (module.context !== this.context) {
326+
throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
327+
}
328+
if (module.status === 'errored') {
329+
throw new ERR_VM_MODULE_LINK_FAILURE(`request for '${specifier}' resolved to an errored module`, module.error);
330+
}
331+
if (module.status === 'unlinked') {
332+
await module[kLink](linker);
333+
}
334+
return module[kWrap];
335+
});
336+
modulePromises[idx] = modulePromise;
337+
specifiers[idx] = specifier;
338+
}
316339

317-
try {
318-
if (promises !== undefined) {
319-
await SafePromiseAllReturnVoid(promises);
320-
}
321-
} catch (e) {
322-
this.#error = e;
323-
throw e;
324-
} finally {
325-
this.#statusOverride = undefined;
326-
}
327-
};
328-
329-
this[kDependencySpecifiers] = undefined;
340+
try {
341+
const modules = await SafePromiseAllReturnArrayLike(modulePromises);
342+
this[kWrap].link(specifiers, modules);
343+
} catch (e) {
344+
this.#error = e;
345+
throw e;
346+
} finally {
347+
this.#statusOverride = undefined;
348+
}
330349
}
331350

332351
get dependencySpecifiers() {
333352
validateInternalField(this, kDependencySpecifiers, 'SourceTextModule');
334-
this[kDependencySpecifiers] ??= ObjectFreeze(this[kWrap].getStaticDependencySpecifiers());
353+
// TODO(legendecas): add a new getter to expose the import attributes as the value type
354+
// of [[RequestedModules]] is changed in https://tc39.es/proposal-import-attributes/#table-cyclic-module-fields.
355+
this[kDependencySpecifiers] ??= ObjectFreeze(
356+
ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => request.specifier));
335357
return this[kDependencySpecifiers];
336358
}
337359

@@ -393,10 +415,10 @@ class SyntheticModule extends Module {
393415
context,
394416
identifier,
395417
});
418+
}
396419

397-
this[kLink] = () => this[kWrap].link(() => {
398-
assert.fail('link callback should not be called');
399-
});
420+
[kLink]() {
421+
/** nothing to do for synthetic modules */
400422
}
401423

402424
setExport(name, value) {

src/env_properties.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
V(args_string, "args") \
7575
V(asn1curve_string, "asn1Curve") \
7676
V(async_ids_stack_string, "async_ids_stack") \
77+
V(attributes_string, "attributes") \
7778
V(base_string, "base") \
7879
V(bits_string, "bits") \
7980
V(block_list_string, "blockList") \
@@ -307,6 +308,7 @@
307308
V(sni_context_string, "sni_context") \
308309
V(source_string, "source") \
309310
V(source_map_url_string, "sourceMapURL") \
311+
V(specifier_string, "specifier") \
310312
V(stack_string, "stack") \
311313
V(standard_name_string, "standardName") \
312314
V(start_time_string, "startTime") \
@@ -384,6 +386,7 @@
384386
V(js_transferable_constructor_template, v8::FunctionTemplate) \
385387
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \
386388
V(message_port_constructor_template, v8::FunctionTemplate) \
389+
V(module_wrap_constructor_template, v8::FunctionTemplate) \
387390
V(microtask_queue_ctor_template, v8::FunctionTemplate) \
388391
V(pipe_constructor_template, v8::FunctionTemplate) \
389392
V(promise_wrap_template, v8::ObjectTemplate) \

0 commit comments

Comments
 (0)