Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions benchmark/esm/startup-esm-graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const common = require('../common.js');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const tmpdir = require('../../test/common/tmpdir');

const BRANCHING_FACTOR = 10;
const benchmarkDirectory = tmpdir.resolve('esm-graph');

// Each measured iteration has to load a graph the ESM cache has never seen, so the
// fixture holds one independent copy of the graph per iteration.
function graphDirectory(modules, copy) {
return path.join(benchmarkDirectory, `g${modules}-${copy}`);
}

function entryURL(modules, copy) {
return pathToFileURL(path.join(graphDirectory(modules, copy), 'mod0.mjs')).href;
}

// Build a complete BRANCHING_FACTOR-ary tree of `modules` + 1 modules rooted at mod0:
// module i imports modules BRANCHING_FACTOR*i+1 through BRANCHING_FACTOR*i+BRANCHING_FACTOR,
// capped at the total, so the shape approximates a real dependency tree rather than
// one module with hundreds of direct imports.
function createGraph(modules, copy) {
const dir = graphDirectory(modules, copy);
fs.mkdirSync(dir, { recursive: true });
const total = modules + 1;
for (let i = 0; i < total; i++) {
let source = '';
for (let c = 1; c <= BRANCHING_FACTOR; c++) {
const child = BRANCHING_FACTOR * i + c;
if (child < total) {
source += `import './mod${child}.mjs';\n`;
}
}
source += `export const value${i} = ${i};\n`;
fs.writeFileSync(path.join(dir, `mod${i}.mjs`), source);
}
}

const bench = common.createBenchmark(main, {
modules: [250, 500, 1000, 2000],
n: [10],
}, {
setup(configs) {
// Build every fixture once here rather than per configuration: writing tens of
// thousands of files is far more expensive than the work being measured.
tmpdir.refresh();
const maxN = configs.reduce((max, c) => Math.max(max, c.n), 0);
for (const modules of new Set(configs.map((c) => c.modules))) {
for (let copy = 0; copy < maxN; copy++) {
createGraph(modules, copy);
}
}
},
});

async function main({ n, modules }) {
bench.start();
for (let i = 0; i < n; i++) {
await import(entryURL(modules, i));
}
bench.end(n);
}
52 changes: 48 additions & 4 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
PromisePrototypeThen,
RegExpPrototypeSymbolReplace,
encodeURIComponent,
globalThis,
hardenRegExp,
} = primordials;

Expand All @@ -22,6 +23,11 @@ const {
kRequiredModuleSymbol,
} = require('internal/modules/cjs/loader');
const { imported_cjs_symbol } = internalBinding('symbols');
const {
privateSymbols: {
entry_point_module_private_symbol,
},
} = internalBinding('util');

const assert = require('internal/assert');
const {
Expand Down Expand Up @@ -350,6 +356,39 @@ class ModuleLoader {
return { wrap: job.module, namespace: job.runSync(parent).namespace };
}

/**
* Load and evaluate the entry point module.
* The graph is always loaded and linked synchronously. When it contains no top-level
* await it is evaluated synchronously too, so startup creates no promises at all.
* When it does contain top-level await, evaluation falls back to `job.run()` here,
* reusing the job that was just built rather than making the caller re-enter the
* loader and redo the resolution and linking for the same module.
* @param {string} url The URL of the entry point module.
* @returns {ModuleJobSync|undefined} The job still needing asynchronous evaluation when
* the graph has top-level await, otherwise `undefined` - the module is then already
* fully evaluated and no promise was ever created.
*/
importForEntryPoint(url) {
return onImport.traceSync(() => {
const request = { specifier: url, phase: kEvaluationPhase, attributes: kEmptyObject, __proto__: null };
const job = this.getOrCreateModuleJob(undefined, request, kImportInImportedESM);
job.instantiateSync();
if (job.module.hasAsyncGraph) {
// Hand the job back so the caller can evaluate it through the ordinary
// asynchronous entry point machinery, without re-resolving and re-linking
// the module the way re-entering `import()` would.
return job;
}
globalThis[entry_point_module_private_symbol] = job.module;
job.runSync();
return undefined;
}, {
__proto__: null,
parentURL: undefined,
url,
});
}

/**
* Check invariants on a cached module job when require()'d from ESM.
* @param {string} specifier The first parameter of require().
Expand Down Expand Up @@ -572,10 +611,14 @@ class ModuleLoader {
assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`);
}

// TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
const ModuleJobCtor = (requestType === kImportInRequiredESM ? ModuleJobSync : ModuleJob);
const isMain = (parentURL === undefined);
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
// Use ModuleJobSync whenever we're on the main thread (not the async loader hook worker),
// except for kRequireInImportedCJS (TODO: consolidate that case too).
// TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
const ModuleJobCtor = (this.isForAsyncLoaderHookWorker ||
requestType === kRequireInImportedCJS) ?
ModuleJob : ModuleJobSync;
job = new ModuleJobCtor(
this,
url,
Expand All @@ -602,8 +645,9 @@ class ModuleLoader {
*/
getOrCreateModuleJob(parentURL, request, requestType) {
let maybePromise;
if (requestType === kRequireInImportedCJS || requestType === kImportInRequiredESM) {
// In these two cases, resolution must be synchronous.
if (!this.isForAsyncLoaderHookWorker) {
// On the main thread, always resolve synchronously;
// `resolveSync` coordinates with the async loader hook worker if needed.
maybePromise = this.resolveSync(parentURL, request);
assert(!isPromise(maybePromise));
} else {
Expand Down
Loading
Loading