#52873: perf: boot every Node.js-hosting process from the Node snapshot / code cache
Description of Change
Since #51703 the browser process boots from the embedded Node.js startup snapshot on native builds, but every other process that hosts Node.js - renderers with Node integration or an unsandboxed preload, Node-enabled web workers, the utility-process node service, and ELECTRON_RUN_AS_NODE / child_process.fork() children - still ran the full bootstrap, compiling the ~120-140 builtins it loads from source on every start (and then serializing a fresh code cache for each of them that nothing ever consumed: NODE_DEBUG_NATIVE=CODE_CACHE in a forked child prints 124× "Compiling … without code cache").
Three pieces:
- Build-time code cache for Node's own builtins in every process flavor.
electron_natives_codecachenow also emits a V8 code cache for Node'slib/builtins (everything butinternal/deps/*, which nothing loads during startup) for the flavors whose processes bootstrap Node from scratch, compiled against that flavor's isolate and flag set exactly like theelectron/js2c/*bundles already were. The cache has to be in place beforenode::NewContext/node::CreateEnvironmentrun the bootstrap - i.e. before we can reachenv->builtin_loader()- so it is installed as a process-wide default that everyBuiltinLoaderis seeded with (small newBuiltinLoader::SetProcessDefaultCodeCache()hook in the existing js2c code-cache node patch), fromJavascriptEnvironment,NodeBindings::Initializeand the web-worker path. GN decides per flavor whether Node's builtins are embedded (always for the renderer flavor; utility/browser only on builds without a Node snapshot), so native builds carry one copy. ELECTRON_RUN_AS_NODEchildren and the utility-process node service create their isolate from the embedded Node snapshot too. Each has exactly oneJavascriptEnvironmentisolate (worker threads inherit the snapshot throughIsolateData), soJavascriptEnvironmenthands them the snapshot like it does for the browser process,CreateIsolateDatagets itsEmbedderSnapshotData,CreateEnvironmentan empty context, the deserialized context is entered, and the duplicate per-isolate message listener the snapshot path already installed is skipped. Cross builds and custom-V8-snapshot setups keep the from-scratch path. The js2c cache flavor now follows what the isolate was actually created from (ELECTRON_RUN_AS_NODEchildren were previously fed the browser flavor, keyed to the Node-snapshot isolate, so even theirnode_initcache was silently rejected).- A
node_platform.ccfix that (2) exposed.WorkerThreadsTaskRunner::DelayedTaskSchedulerdrains its queue through aTaskQueue, which is astd::priority_queuetreating plainv8::Tasks as equal, so aStopTaskcan run before an earlier-queuedScheduleTask; that then arms a timer on the stopped loop andNodePlatform::Shutdown()waits it out - observed as ~10 % of forked children stalling for exactly 8 s (V8's memory-reducer delay) at exit in release builds. New small patch dropsScheduleTasks that arrive afterStopTask; I'll send it upstream as well.
Whether a process may use the Node snapshot is now decided only by comparing the blob it loaded against the built-in one (#52872), so the LoadBrowserProcessSpecificV8Snapshot fuse affects just the browser process, whose blob it swaps.
Tests: the DCHECK-only cache-status record now covers all builtins, and spec/api-js2c-code-cache-spec.ts asserts that browser / renderer / utility / run-as-node compile no Node builtin without consuming a cache entry, once as built and once from a copy of the app with the LoadBrowserProcessSpecificV8Snapshot fuse on. node-spec, utility-process, net, modules, asar, esm, context-bridge and ipc-renderer specs pass locally.
Linux x64 release build (official, ThinLTO, PGO), interleaved A/B against main, n=40 fresh processes per cell:
main |
this PR | |
|---|---|---|
ELECTRON_RUN_AS_NODE=1 electron -e 0 (spawn → exit) |
82.5 ms | 37.0 ms |
child_process.fork(noop) → 'exit' |
89.4 ms | 41.1 ms |
utilityProcess.fork() → first message |
86.1 ms | 39.7 ms |
nodeIntegration renderer, timeOrigin → first preload statement |
90.9 ms | 64.7 ms |
| stripped binary | +2.0 MB |
(The +2.0 MB is the renderer flavor's cache for Node's builtins; a testing/dcheck build shows +6.4 MB because the caches serialize ~3.5× larger there.)
Checklist
- PR description included and stakeholders cc'd
-
npm testpasses - tests are changed or added
Release Notes
Notes: Improved startup time of utilityProcess.fork(), child_process.fork() / ELECTRON_RUN_AS_NODE child processes and Node.js-enabled renderers.
Backports
Semver Impact
Semantic Versioning helps users understand the impact of updates:
- Major (X.y.z): Breaking changes that may require code modifications
- Minor (x.Y.z): New features that maintain backward compatibility
- Patch (x.y.Z): Bug fixes that don't change the API
- None: Changes that don't affect using facing parts of Electron