worker: start worker threads from the built-in snapshot - #65336
Open
codebytere wants to merge 1 commit into
Open
worker: start worker threads from the built-in snapshot#65336codebytere wants to merge 1 commit into
codebytere wants to merge 1 commit into
Conversation
Collaborator
|
Review requested:
|
codebytere
marked this pull request as ready for review
August 16, 2026 20:50
codebytere
force-pushed
the
worker/start-from-snapshot
branch
from
August 16, 2026 21:05
f15c413 to
f7e1092
Compare
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 built-in (kDefault) snapshot is in use, 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
force-pushed
the
worker/start-from-snapshot
branch
from
August 16, 2026 21:05
f7e1092 to
b55b8e2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
new Worker()currently runs the whole internal bootstrap in the new isolate (realm, node, web exposure, the thread andprocess-state switches), compiling ~80 builtins with the code cache before
internal/main/worker_threadstarts. Only themain 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.jsanddoes_not_own_process_state.jsare written as overrides of the main-thread switches, and the only thread-specific databaked into the context were six properties of the
workerbinding. This lets a worker deserialize the samekNodeMainContextIndexcontext andEnvSerializeInfothe main thread uses, and applies the two worker-side switches ontop.
(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.)
workerbinding:threadId,threadName,isMainThread,isInternalThread,ownsProcessStateandresourceLimitsbecome lazy properties of the per-isolate template, computed from the
Environmenton first read, so no bootstrappedcontext carries them. Bootstrap itself only takes
getEnvMessagePortfrom the binding.CreateEnvironment(): an empty context already means "deserialize from the snapshot". When the caller is a worker (itsIsolateDatahas aWorker), deserialize the main context as before, then runinternal/bootstrap/switches/is_not_main_threadand, unless the worker owns process state,does_not_own_process_stateafterInitializeMainContext(). The isolate error-handler reset stays main-thread only.Worker::Run(): take that path when the built-in snapshot is in use (SnapshotMetadata::Type::kDefault, so a--build-snapshotsnapshot that has run the application's entry point is never reused), browser globals aren't disabled(
kNoBrowserGlobalschanges the bootstrap shape) and--no-worker-snapshotwasn't passed. Otherwise the workerbootstraps from scratch as today, which is also the path for embedders that create environments without a snapshot.
is_not_main_thread.js: also deleteprocess._debugPause,_startProfilerIdleNotifierand_stopProfilerIdleNotifier, whichis_main_thread.jsinstalls.--[no-]worker-snapshotper-isolate option, documented, as the escape hatch.Modules read no options at bootstrap time (
getCLIOptionsValues()throws before bootstrapping is done andrefreshOptions()runs in pre-execution), so a worker'sexecArgvcan't disagree with anything captured in the context;per-thread runtime state (
argv,execArgv, title, env proxy contents, time origin, inspector, message port, stdio) isestablished after context creation by
Worker::Run()andprepareWorkerThreadExecution()as before.Testing:
Object.getOwnPropertyNames()plus descriptor kinds ofprocessandglobalThisare identical between a from-snapshotand a
--no-worker-snapshotworker, as areprocesslisteners,featuresandversions.mainforenv: SHARE_ENVand copied env,resourceLimits, piped stdio, nested workers, eval/CJS/ESM/data:entries, exit codes, earlyterminate(), uncaught errors, structured-cloneworkerData,argv/execArgv/name,--frozen-intrinsics,--disable-proto=throw, the permission model, and a--build-snapshotuser snapshot (workers donot see its globals).
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_mksnapshotisolate currently fails inside V8's serializer (SerializeBackingStore()on a typed array reached fromboth contexts, or
CHECK(!SerializePendingObject(*code))inVisitJSDispatchTableEntry()), so reusing the existingcontext 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.