Files
electron/shell/common/node_natives_code_cache.cc
Shelley Vohr a77b899ec9 perf: boot every Node.js-hosting process from the Node snapshot / code cache (#52873)
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 consumed).

Three pieces:

1. Build-time code cache for Node's own builtins in every process flavor.
   The electron_natives_codecache tool now also emits a V8 code cache for
   Node's lib/ builtins (all but internal/deps/*) for the flavors whose
   processes bootstrap Node from scratch, compiled against that flavor's
   isolate and flag set like the electron/js2c/* bundles already were.
   The cache has to be in place before node::NewContext and
   node::CreateEnvironment run the bootstrap, i.e. before Electron can
   reach env->builtin_loader(), so it is installed as a process-wide
   default every BuiltinLoader is seeded with (new
   BuiltinLoader::SetProcessDefaultCodeCache() hook in the js2c code-cache
   node patch), from JavascriptEnvironment / NodeBindings::Initialize /
   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 pay for one copy.

2. ELECTRON_RUN_AS_NODE children and the utility-process node service
   create their isolate from the embedded Node snapshot too. Each has
   exactly one JavascriptEnvironment isolate (worker threads inherit the
   snapshot through IsolateData), so JavascriptEnvironment hands them the
   snapshot like the browser process, CreateIsolateData gets its
   EmbedderSnapshotData, CreateEnvironment an 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_NODE children were previously fed the browser flavor
   keyed to the Node-snapshot isolate, so even their node_init cache was
   silently rejected).

3. A node_platform fix this exposed: WorkerThreadsTaskRunner's
   DelayedTaskScheduler drains its queue through a TaskQueue, which is a
   std::priority_queue treating plain v8::Tasks as equal, so a StopTask
   can run before an earlier-queued ScheduleTask; that then arms a timer
   on the stopped loop and NodePlatform::Shutdown() waits it out --
   observed as ~10 % of forked children stalling exactly 8 s (V8's
   memory-reducer delay) at exit in release builds. New patch drops
   ScheduleTasks that arrive after StopTask; upstreamable.

The DCHECK-only cache-status record now covers all builtins, and the
js2c code-cache spec 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 enabled. Whether a process may
use the Node snapshot is decided by comparing the blob it loaded with the
built-in one alone, so the fuse only affects the process whose blob it
actually swaps.
spec/node-spec.ts, utility-process, net, modules, asar, esm,
context-bridge, ipc-renderer specs pass locally.

Linux x64 release build (official, ThinLTO, PGO), interleaved A/B vs
main, n=40 fresh processes per cell:
  ELECTRON_RUN_AS_NODE=1 electron -e 0     82.5 -> 37.0 ms
  child_process.fork(noop) -> 'exit'       89.4 -> 41.1 ms
  utilityProcess.fork -> first message     86.1 -> 39.7 ms
  nodeIntegration renderer, timeOrigin ->
    first preload statement                90.9 -> 64.7 ms
  stripped binary                          +2.0 MB
2026-08-17 22:04:35 +02:00

59 lines
2.4 KiB
C++

// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/node_natives_code_cache.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_natives_code_cache_internal.h"
#include "shell/common/process_util.h"
#include "third_party/electron_node/src/node_snapshot_builder.h"
namespace electron {
Js2cCacheFlavor CurrentProcessJs2cCacheFlavor(bool has_node_env) {
if (IsBrowserProcess() || IsUtilityProcess()) {
// Processes that host Node through a JavascriptEnvironment: the browser
// process, ELECTRON_RUN_AS_NODE children (no --type either) and the node
// service's utility process. On builds that embed a Node startup snapshot
// their isolate normally comes from that snapshot, which is what the
// browser flavor is keyed to there. When such a process nevertheless boots
// from the v8 context snapshot (a custom V8 snapshot is loaded), its
// isolate and flag set match the utility flavor instead (which then lacks
// browser_init; it compiles from source). Without an embedded snapshot both
// flavors are keyed to the v8 context snapshot and split by init bundle.
if (JavascriptEnvironment::NodeSnapshot())
return Js2cCacheFlavor::kBrowser;
const bool build_embeds_node_snapshot =
node::SnapshotBuilder::GetEmbeddedSnapshotData() != nullptr;
return IsUtilityProcess() || build_embeds_node_snapshot
? Js2cCacheFlavor::kUtility
: Js2cCacheFlavor::kBrowser;
}
if (IsRendererProcess()) {
// No Node env means the V8 flag set stays frozen -- a distinct flavor.
return has_node_env ? Js2cCacheFlavor::kRenderer
: Js2cCacheFlavor::kSandbox;
}
return Js2cCacheFlavor::kWorker;
}
const std::vector<node::builtins::CodeCacheInfo>& GetNativesCodeCache(
Js2cCacheFlavor flavor) {
switch (flavor) {
case Js2cCacheFlavor::kSandbox:
return internal::Js2cCacheSandbox();
case Js2cCacheFlavor::kRenderer:
return internal::Js2cCacheRenderer();
case Js2cCacheFlavor::kBrowser:
return internal::Js2cCacheBrowser();
case Js2cCacheFlavor::kUtility:
return internal::Js2cCacheUtility();
case Js2cCacheFlavor::kWorker:
return internal::Js2cCacheWorker();
}
}
} // namespace electron