Skip to content

worker: start worker threads from the built-in snapshot - #65336

Open
codebytere wants to merge 1 commit into
nodejs:mainfrom
codebytere:worker/start-from-snapshot
Open

worker: start worker threads from the built-in snapshot#65336
codebytere wants to merge 1 commit into
nodejs:mainfrom
codebytere:worker/start-from-snapshot

Conversation

@codebytere

@codebytere codebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

new Worker() currently runs the whole internal bootstrap in the new isolate (realm, node, web exposure, the thread and process-state switches), compiling ~80 builtins with the code cache before internal/main/worker_thread starts. Only the main thread deserializes its principal context from the built-in snapshot, and that bootstrap is about half of a worker's cold start.

The bootstrapped principal context in the snapshot is nearly thread-neutral already: is_not_main_thread.js and does_not_own_process_state.js are written as overrides of the main-thread switches, and the only thread-specific data baked into the context were six properties of the worker binding. This lets a worker deserialize the same kNodeMainContextIndex context and EnvSerializeInfo the main thread uses, and applies the two worker-side switches on top.

misc/startup-core.js mode='worker' script='test/fixtures/semicolon.js'                ***    98.45 %  ±0.82%   (21.1 -> 10.6 ms)
misc/startup-core.js mode='worker' script='benchmark/fixtures/empty.mjs'              ***    99.64 %  ±0.60%
misc/startup-core.js mode='worker' script='benchmark/fixtures/require-builtins.js'    ***    47.58 %  ±1.19%   (33.8 -> 22.9 ms)
misc/startup-core.js mode='worker' script='benchmark/fixtures/import-builtins.mjs'    ***    30.03 %  ±0.54%
misc/startup-core.js mode='worker' script='test/fixtures/snapshot/typescript.js'      ***     8.59 %  ±0.46%
misc/startup-core.js mode='process' (all five scripts)                                        ±0.5 %  n.s.

(x64 Linux, 30 runs. An idle worker's RSS also drops from ~12.2 to ~8.1 MiB, heapTotal 8.6 to 5.4 MiB.)

  • worker binding: threadId, threadName, isMainThread, isInternalThread, ownsProcessState and resourceLimits become lazy properties of the per-isolate template, computed from the Environment on first read, so no bootstrapped context carries them. Bootstrap itself only takes getEnvMessagePort from the binding.
  • CreateEnvironment(): an empty context already means "deserialize from the snapshot". When the caller is a worker (its IsolateData has a Worker), deserialize the main context as before, then run internal/bootstrap/switches/is_not_main_thread and, unless the worker owns process state, does_not_own_process_state after InitializeMainContext(). The isolate error-handler reset stays main-thread only.
  • Worker::Run(): take that path only when the snapshot in use is the embedded one (an embedder's own snapshot or a --snapshot-blob one has run application code in its main context, so it is never reused), browser globals aren't disabled (kNoBrowserGlobals changes the bootstrap shape) and --no-worker-snapshot wasn't passed. Otherwise the worker bootstraps from scratch as today, which is also the path for embedders that create environments without a snapshot.
  • is_not_main_thread.js: also delete process._debugPause, _startProfilerIdleNotifier and _stopProfilerIdleNotifier, which is_main_thread.js installs.
  • --[no-]worker-snapshot per-isolate option, documented, as the escape hatch.

Modules read no options at bootstrap time (getCLIOptionsValues() throws before bootstrapping is done and refreshOptions() runs in pre-execution), so a worker's execArgv can't disagree with anything captured in the context; per-thread runtime state (argv, execArgv, title, env proxy contents, time origin, inspector, message port, stdio) is established after context creation by Worker::Run() and prepareWorkerThreadExecution() as before.

Testing:

  • Object.getOwnPropertyNames() plus descriptor kinds of process and globalThis are identical between a from-snapshot and a --no-worker-snapshot worker, as are process listeners, features and versions.
  • Same results as main for env: SHARE_ENV and copied env, resourceLimits, piped stdio, nested workers, eval/CJS/ESM/data: entries, exit codes, early terminate(), uncaught errors, structured-clone workerData, argv/execArgv/name, --frozen-intrinsics, --disable-proto=throw, the permission model, and a --build-snapshot user snapshot (workers do not see its globals).
  • Full default suite on Release; worker, messageport, broadcastchannel, inspector-worker, async-hooks, process, bootstrap, snapshot and es-module suites on a Debug build (437/437).

An earlier version added a dedicated worker context to the snapshot instead. Any second bootstrapped context in the node_mksnapshot isolate currently fails inside V8's serializer (SerializeBackingStore() on a typed array reached from both contexts, or CHECK(!SerializePendingObject(*code)) in VisitJSDispatchTableEntry()), so reusing the existing context is both the smaller change and the one that works today; i'll file the serializer limitation separately.


Disclosure: the code, tests, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/config
  • @nodejs/startup

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebytere codebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@codebytere
codebytere marked this pull request as ready for review August 16, 2026 20:50
@codebytere
codebytere force-pushed the worker/start-from-snapshot branch 2 times, most recently from f7e1092 to b55b8e2 Compare August 16, 2026 21:05
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.76471% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.16%. Comparing base (30bff4a) to head (a0d71cf).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
src/api/environment.cc 61.53% 2 Missing and 3 partials ⚠️
src/node_worker.cc 96.77% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65336      +/-   ##
==========================================
+ Coverage   90.13%   90.16%   +0.03%     
==========================================
  Files         752      752              
  Lines      251568   251649      +81     
  Branches    47270    47290      +20     
==========================================
+ Hits       226759   226909     +150     
+ Misses      16168    16092      -76     
- Partials     8641     8648       +7     
Files with missing lines Coverage Δ
.../internal/bootstrap/switches/is_not_main_thread.js 100.00% <100.00%> (ø)
src/node_options.cc 79.50% <100.00%> (-0.04%) ⬇️
src/node_options.h 95.37% <100.00%> (+0.02%) ⬆️
src/node_worker.h 91.66% <ø> (ø)
src/node_worker.cc 82.16% <96.77%> (+0.65%) ⬆️
src/api/environment.cc 77.74% <61.53%> (-0.68%) ⬇️

... and 55 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/node_worker.cc Outdated
Comment thread src/node_worker.cc Outdated
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(); skip the isolate error-handler reset.
- 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.
- --[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>
@codebytere
codebytere force-pushed the worker/start-from-snapshot branch from b55b8e2 to a0d71cf Compare August 17, 2026 05:16
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 17, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants