3 Commits
Author SHA1 Message Date
Shelley Vohr a834c9beab refactor: register Electron's js2c bundles as internal Node builtins (#53547)
Emit the bundles under gen/internal/electron/js2c so js2c gives them
internal/electron/js2c/* ids: they leave module.builtinModules, cannot be
required or fetched with process.getBuiltinModule() from app code (which
crashed under ELECTRON_RUN_AS_NODE), and parallel/test-process-get-builtin is
re-enabled.
2026-09-06 19:15:18 +02:00
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
Samuel Attard d8571176d3 perf: build-time V8 code cache for the electron/js2c framework bundles (#51697)
* build: dedupe get-intrinsic to shrink the sandboxed renderer bundle

The hoisted get-intrinsic resolved to 1.2.1 (satisfying the older
^1.0.2..^1.2.0 consumers), so side-channel-weakmap, side-channel-map,
call-bound and es-set-tostringtag — which need ^1.2.5/^1.3.0 — each
pulled a nested get-intrinsic@1.3.0 copy. webpack bundled three of
those nested copies into sandbox_bundle.js (~16.5 KB each).

1.3.0 satisfies every range in the tree (all ^1.x), so pin it via
resolutions to collapse to a single copy. Drops sandbox_bundle.js
from 364,762 to 323,116 bytes (-41,646, -11.4%) — that bundle is
parsed/preparsed in every sandboxed renderer before the HTML parser,
so this is a direct per-renderer startup win on top of the IPC and
code-cache changes.

* perf: build-time V8 code cache for the electron/js2c framework bundles

Generate a V8 code cache for Electron's embedded electron/js2c/* bundles
at build time and embed it in the binary, so each process deserializes
its framework bundle (kConsumeCodeCache) instead of parsing and compiling
it from source at startup.

V8's code-cache key includes FlagList::Hash() over non-default flags and
the snapshot's read-only checksum, both of which differ per process type,
so one cache variant is generated per flavor and each process consumes
the matching one:

  sandbox  : sandboxed renderer, no Node env  (+SAB)
  renderer : normal renderer, has Node env    (+SAB +rehash +no-freeze)
  browser  : main process                     (+rehash +no-freeze)
  utility  : utility process                  (+rehash +no-freeze)
  worker   : node/web worker                  (+rehash +no-freeze)

All flavors create their isolate from the v8 context snapshot. The flag
deltas are deterministic; --no-freeze-flags-after-init (present iff the
process has a Node Environment) lets V8 recompute the flag-hash after
init, so the consume-time hash of every node-env process includes
--rehash-snapshot. A sandboxed renderer has no Node env (flags stay
frozen) so it is a distinct flavor, discriminated at runtime by
node::Environment::GetCurrent(context).

The generator compiles each bundle with kEagerCompile so the cache
covers the inner functions, not just the top-level wrapper -- the
framework bundles run in full at startup, so this front-loads the lazy
compiles to build time. Cross-arch is supported (the generator is built
in v8_snapshot_toolchain so it links V8 with V8_TARGET_ARCH set to the
target and reads the target's snapshot blob); cross-OS is untested and
gated off.

- electron_natives_codecache_main.cc: per-flavor build-time generator
  compiling each bundle through the same BuiltinLoader path its runtime
  consumer uses.
- shell/common/js2c_bundle_ids.h: shared bundle ids and wrapper-function
  param names used by the generator and the runtime CompileAndCall call
  sites, so the source/params hash matches by construction.
- node_util.cc feeds the standalone loader; FeedEnvironmentCodeCache
  feeds the per-Environment loader before LoadEnvironment so the *_init
  bundles are consumed.
- Test-only (DCHECK builds, compiled out of production): a per-process
  id->accepted status map exposed via v8_util.getJs2cCodeCacheStatus, a
  sandboxed-preload shim, and spec/api-js2c-code-cache-spec.ts which
  spawns a clean app and asserts every bundle is consumed in
  browser/sandbox/renderer/utility.

DidClearWindowObject (sandboxed renderer, pre-HTML-parser): ~9.8 ms
(cache rejected/absent) -> ~6.4 ms (eager cache consumed), a ~3.4 ms /
~35% cold-start win on every launch. No V8 patch.

* chore: export node patch for the js2c build-time code cache
2026-05-20 19:51:30 +00:00