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 involving general changes in the lib/ or src/ directories. 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.56627% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.15%. Comparing base (a844473) to head (90320cf).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/api/environment.cc 66.66% 2 Missing and 2 partials ⚠️
src/node_worker.cc 95.16% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65336      +/-   ##
==========================================
+ Coverage   90.07%   90.15%   +0.07%     
==========================================
  Files         754      754              
  Lines      256378   256418      +40     
  Branches    48498    48507       +9     
==========================================
+ Hits       230936   231163     +227     
+ Misses      16572    16369     -203     
- Partials     8870     8886      +16     
Files with missing lines Coverage Δ
.../internal/bootstrap/switches/is_not_main_thread.js 100.00% <100.00%> (ø)
src/node_options.cc 79.66% <100.00%> (+0.03%) ⬆️
src/node_options.h 95.39% <100.00%> (+0.48%) ⬆️
src/node_worker.h 91.66% <ø> (ø)
src/node_worker.cc 82.24% <95.16%> (+0.24%) ⬆️
src/api/environment.cc 78.18% <66.66%> (-0.06%) ⬇️

... and 47 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
@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 nodejs deleted a comment from nodejs-github-bot Aug 23, 2026
@nodejs nodejs deleted a comment from nodejs-github-bot Aug 23, 2026
@nodejs nodejs deleted a comment from nodejs-github-bot Aug 23, 2026
@nodejs nodejs deleted a comment from nodejs-github-bot Aug 23, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs nodejs deleted a comment from nodejs-github-bot Aug 25, 2026
@nodejs nodejs deleted a comment from nodejs-github-bot Aug 25, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@joyeecheung joyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply overriding the main thread snapshot sounds like a brilliant idea, thanks :) LGTM with the CI happy.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebytere codebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 30, 2026
@nodejs-github-bot nodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 30, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Commit Queue failed

   ✘  GitHub CI failed with status: FAILURE
   ✘  2 failure(s) on the last Jenkins CI run

The pull request was removed from the Commit Queue and labeled commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. . After resolving the failure, remove that label and add commit-queue PRs queued for automated landing through the Commit Queue. to retry.

Full Commit Queue output
- Loading data for nodejs/node/pull/65336
✔  Done loading data for nodejs/node/pull/65336
----------------------------------- PR info ------------------------------------
Title      worker: start worker threads from the built-in snapshot (#65336)
Author     Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch     codebytere:worker/start-from-snapshot -> nodejs:main
Labels     c++, lib / src, needs-ci, commit-queue
Commits    1
 - worker: start worker threads from the built-in snapshot
Committers 1
 - 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>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65336
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
--------------------------------------------------------------------------------
   ℹ  This PR was created on Sun, 16 Aug 2026 20:28:29 GMT
   ✔  Approvals: 2
   ✔  - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65336#pullrequestreview-4948973600
   ✔  - Joyee Cheung (@joyeecheung) (TSC): https://github.com/nodejs/node/pull/65336#pullrequestreview-5053813144
   ✘  GitHub CI failed with status: FAILURE
   ℹ  Last Full PR CI on 2026-08-29T17:58:33Z: https://ci.nodejs.org/job/node-test-pull-request/76702/
- Querying data for job/node-test-pull-request/76702/
✔  Build data downloaded
- Querying failures of job/node-test-commit/91451/
✔  Data downloaded
   ✘  2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
   ✔  Aborted `git node land` session in /home/runner/work/node/node/.ncu

View workflow run

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

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 a0d71cf to 90320cf Compare August 31, 2026 16:31
@codebytere codebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2026
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++. commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. request-ci Add this label to start a Jenkins CI on a PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants