Files
node/test/parallel/test-bootstrap-modules.js
Shelley Vohr ddc0a0aa28 worker: start worker threads from the built-in snapshot
Every `new Worker()` runs the whole internal bootstrap (realm, node, web
exposure, thread and process-state switches) in its fresh isolate,
compiling ~80 builtins with the code cache; only the main thread
deserializes its principal context from the built-in snapshot. That
bootstrap is about half of a worker's cold start.

The bootstrapped principal context in the snapshot is nearly
thread-neutral: the worker-side switch scripts (is_not_main_thread,
does_not_own_process_state) are written as overrides of the main-thread
ones, and the per-thread values of the `worker` binding were the only
thread-specific data baked into the context. Let a worker deserialize
that same context and EnvSerializeInfo and apply the two worker-side
switches on top:

- worker binding: threadId, threadName, isMainThread, isInternalThread,
  ownsProcessState and resourceLimits become lazy properties of the
  per-isolate template, computed from the Environment on first read.
- CreateEnvironment(): when a worker (its IsolateData has a Worker)
  passes an empty context, deserialize kNodeMainContextIndex and run
  internal/bootstrap/switches/is_not_main_thread and, unless the worker
  owns process state, does_not_own_process_state after
  InitializeMainContext().
- Worker::Run(): take that path when the embedded built-in snapshot is
  in use (not an embedder's or a --snapshot-blob one, which has run
  application code), browser globals are not disabled and
  --no-worker-snapshot was not given; otherwise bootstrap as before.
- is_not_main_thread.js: also delete _debugPause and the profiler idle
  notifier helpers that is_main_thread.js installs.
- pre_execution: run the snapshot's deserialize callbacks (Buffer pool,
  default resolver, cached cwd) on worker threads too, now that a worker
  can come from a snapshot.
- --[no-]worker-snapshot per-isolate option, documented.

Sequential new Worker() -> 'online' -> terminate goes from ~20.9 ms to
~10.3 ms per worker on x64 Linux; --no-worker-snapshot restores the old
number.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65336
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
2026-09-04 13:28:27 +00:00

277 lines
10 KiB
JavaScript

'use strict';
// This list must be computed before we require any builtins to
// to eliminate the noise.
const list = process.moduleLoadList.slice();
const common = require('../common');
const assert = require('assert');
const { inspect } = require('util');
const preExecIndex =
list.findIndex((i) => i.includes('pre_execution'));
const actual = {
beforePreExec: new Set(list.slice(0, preExecIndex)),
atRunTime: new Set(list.slice(preExecIndex)),
};
// Currently, we don't add additional builtins to worker snapshots.
// So for worker snapshots we'll just concatenate the two. Once we
// add more builtins to worker snapshots, we should also distinguish
// the two stages for them.
const expected = {};
const getFormatNativeModule = 'NativeModule internal/modules/esm/get_format';
expected.beforePreExec = new Set([
'Internal Binding builtins',
'Internal Binding modules',
'Internal Binding errors',
'Internal Binding util',
'NativeModule internal/errors',
'Internal Binding config',
'Internal Binding timers',
'Internal Binding async_context_frame',
'NativeModule internal/async_context_frame',
'Internal Binding async_wrap',
'Internal Binding task_queue',
'Internal Binding symbols',
'NativeModule internal/async_hooks',
'Internal Binding constants',
'Internal Binding types',
'NativeModule internal/util',
'NativeModule internal/util/types',
'NativeModule internal/validators',
'NativeModule internal/linkedlist',
'NativeModule internal/priority_queue',
'NativeModule internal/assert',
'NativeModule internal/util/inspect',
'NativeModule internal/util/debuglog',
'NativeModule internal/streams/utils',
'NativeModule internal/timers',
'NativeModule events',
'Internal Binding buffer',
'Internal Binding string_decoder',
'NativeModule util/types',
'NativeModule internal/buffer',
'NativeModule buffer',
'Internal Binding messaging',
'NativeModule internal/worker/js_transferable',
'Internal Binding process_methods',
'NativeModule internal/process/per_thread',
'Internal Binding credentials',
'NativeModule internal/process/promises',
'NativeModule internal/fixed_queue',
'NativeModule async_hooks',
'NativeModule internal/process/task_queues',
'NativeModule timers',
'Internal Binding trace_events',
'NativeModule internal/constants',
'NativeModule path',
'NativeModule internal/process/execution',
'NativeModule internal/process/permission',
'NativeModule internal/process/warning',
'NativeModule internal/console/constructor',
'NativeModule internal/console/global',
'NativeModule internal/querystring',
'NativeModule querystring',
'Internal Binding url',
'Internal Binding url_pattern',
'Internal Binding blob',
'NativeModule internal/url',
'NativeModule util',
'NativeModule internal/webidl',
'Internal Binding performance',
'Internal Binding permission',
'NativeModule internal/perf/utils',
'NativeModule internal/event_target',
'Internal Binding mksnapshot',
'NativeModule internal/v8/startup_snapshot',
'NativeModule internal/process/signal',
'Internal Binding fs',
'NativeModule internal/fs/utils',
'NativeModule fs',
'Internal Binding options',
'NativeModule internal/options',
'NativeModule internal/source_map/source_map_cache',
'Internal Binding contextify',
'NativeModule internal/vm',
'NativeModule internal/modules/helpers',
'NativeModule internal/modules/customization_hooks',
'NativeModule internal/modules/package_json_reader',
'Internal Binding module_wrap',
'NativeModule internal/modules/cjs/loader',
'NativeModule internal/modules/package_map',
'NativeModule diagnostics_channel',
'Internal Binding diagnostics_channel',
'Internal Binding wasm_web_api',
'NativeModule internal/events/abort_listener',
'NativeModule internal/modules/esm/utils',
'Internal Binding worker',
'NativeModule internal/modules/run_main',
getFormatNativeModule,
'NativeModule internal/trace_events',
]);
expected.atRunTime = new Set([
'NativeModule internal/process/pre_execution',
]);
const { isMainThread } = require('worker_threads');
// Binaries built without the snapshot (e.g. cross-compiled) and
// --no-node-snapshot bootstrap the context from scratch; so do workers under
// --no-worker-snapshot.
const contextFromSnapshot =
process.config.variables.node_use_node_snapshot &&
!process.execArgv.includes('--no-node-snapshot') &&
(isMainThread || !process.execArgv.includes('--no-worker-snapshot'));
if (contextFromSnapshot) {
[
'Internal Binding cjs_lexer',
'NativeModule internal/modules/esm/assert',
'NativeModule internal/modules/esm/loader',
'NativeModule internal/modules/esm/load',
'NativeModule internal/modules/esm/resolve',
'NativeModule internal/modules/esm/translators',
'NativeModule internal/modules/esm/module_job',
'NativeModule internal/modules/esm/module_map',
'NativeModule url',
'Internal Binding encoding_binding',
'NativeModule internal/blob',
'NativeModule internal/data_url',
'NativeModule internal/dns/utils',
'NativeModule internal/encoding',
'NativeModule internal/encoding/single-byte',
'NativeModule internal/encoding/util',
'NativeModule internal/mime',
'NativeModule internal/modules/typescript',
'NativeModule internal/net',
].forEach(expected.beforePreExec.add.bind(expected.beforePreExec));
} else if (isMainThread) {
expected.beforePreExec.delete(getFormatNativeModule);
expected.atRunTime.add(getFormatNativeModule);
}
if (!isMainThread) {
[
'NativeModule diagnostics_channel',
'NativeModule internal/abort_controller',
'NativeModule internal/process/worker_thread_only',
'NativeModule internal/streams/add-abort-signal',
'NativeModule internal/streams/compose',
'NativeModule internal/streams/destroy',
'NativeModule internal/streams/duplex',
'NativeModule internal/streams/duplexpair',
'NativeModule internal/streams/end-of-stream',
'NativeModule internal/streams/from',
'NativeModule internal/streams/legacy',
'NativeModule internal/streams/operators',
'NativeModule internal/streams/passthrough',
'NativeModule internal/streams/pipeline',
'NativeModule internal/streams/readable',
'NativeModule internal/streams/state',
'NativeModule internal/streams/transform',
'NativeModule internal/streams/utils',
'NativeModule internal/streams/writable',
'NativeModule internal/worker',
'NativeModule internal/worker/io',
'NativeModule internal/worker/messaging',
'NativeModule stream',
'NativeModule stream/promises',
'NativeModule string_decoder',
'NativeModule worker_threads',
].forEach(expected.atRunTime.add.bind(expected.atRunTime));
// For now we'll concatenate the two stages for workers. We prefer
// atRunTime here because that's what currently happens for these.
}
if (common.isWindows) {
// On Windows fs needs SideEffectFreeRegExpPrototypeExec which uses vm.
expected.atRunTime.add('NativeModule vm');
}
if (common.hasIntl) {
expected.beforePreExec.add('Internal Binding icu');
}
if (process.features.inspector) {
expected.beforePreExec.add('Internal Binding inspector');
expected.beforePreExec.add('NativeModule internal/util/inspector');
expected.beforePreExec.add('NativeModule internal/inspector_async_hook');
}
// This is loaded if the test is run with NODE_V8_COVERAGE.
if (process.env.NODE_V8_COVERAGE) {
expected.atRunTime.add('Internal Binding profiler');
}
// Accumulate all the errors and print them at the end instead of throwing
// immediately which makes it harder to update the test.
const errorLogs = [];
function err(message) {
if (typeof message === 'string') {
errorLogs.push(message);
} else {
// Show the items in individual lines for easier copy-pasting.
errorLogs.push(inspect(message, { compact: false }));
}
}
if (isMainThread) {
const missing = expected.beforePreExec.difference(actual.beforePreExec);
const extra = actual.beforePreExec.difference(expected.beforePreExec);
if (missing.size !== 0) {
err('These builtins are now no longer loaded before pre-execution.');
err('If this is intentional, remove them from `expected.beforePreExec`.');
err('\n--- These could be removed from expected.beforePreExec ---');
err([...missing].sort());
err('');
}
if (extra.size !== 0) {
err('These builtins are now unexpectedly loaded before pre-execution.');
err('If this is intentional, add them to `expected.beforePreExec`.');
err('\n# Note: loading more builtins before pre-execution can lead to ' +
'startup performance regression or invalid snapshots.');
err('- Consider lazy loading builtins that are not used universally.');
err('- Make sure that the builtins do not access environment dependent ' +
'states e.g. command line arguments or environment variables ' +
'during loading.');
err('- When in doubt, ask @nodejs/startup.');
err('\n--- These could be added to expected.beforePreExec ---');
err([...extra].sort());
err('');
}
}
if (!isMainThread) {
// For workers, just merge beforePreExec into atRunTime for now.
// When we start adding modules to the worker snapshot, this branch
// can be removed and we can just remove the isMainThread
// conditions.
expected.beforePreExec.forEach(expected.atRunTime.add.bind(expected.atRunTime));
actual.beforePreExec.forEach(actual.atRunTime.add.bind(actual.atRunTime));
}
{
const missing = expected.atRunTime.difference(actual.atRunTime);
const extra = actual.atRunTime.difference(expected.atRunTime);
if (missing.size !== 0) {
err('These builtins are now no longer loaded at run time.');
err('If this is intentional, remove them from `expected.atRunTime`.');
err('\n--- These could be removed from expected.atRunTime ---');
err([...missing].sort());
err('');
}
if (extra.size !== 0) {
err('These builtins are now unexpectedly loaded at run time.');
err('If this is intentional, add them to `expected.atRunTime`.');
err('\n# Note: loading more builtins at run time can lead to ' +
'startup performance regression.');
err('- Consider lazy loading builtins that are not used universally.');
err('\n--- These could be added to expected.atRunTime ---');
err([...extra].sort());
err('');
}
}
assert.strictEqual(errorLogs.length, 0, errorLogs.join('\n'));