perf: boot the browser process from an embedded Node startup snapshot#51703
Conversation
c463d98 to
7d74860
Compare
ccac851 to
00c0946
Compare
7d74860 to
2f7a53e
Compare
00c0946 to
5e6ed2c
Compare
2f7a53e to
b3e73d2
Compare
|
Trivial: needs a linter patch |
61f3dac to
fec0279
Compare
b3e73d2 to
b59ddd4
Compare
f24bca3 to
8baf30e
Compare
b59ddd4 to
253f909
Compare
8baf30e to
126333a
Compare
253f909 to
f94aa60
Compare
ckerr
left a comment
There was a problem hiding this comment.
Two comments inline. Also GPT 5.5 claims:
ELECTRON_RUN_AS_NODE now crashes on startup. The new gate in javascript_environment.cc treats any process with no Chromium --type as browser-process-only, but ELECTRON_RUN_AS_NODE also has no --type. That makes javascript_environment.cc:40-50 create the isolate from the embedded Node snapshot and skip node::NewContext; then node_main.cc:269-287 still creates IsolateData without passing the snapshot wrapper and calls CreateEnvironment with the current empty context. CI confirms this with Assertion failed: (isolate_data->snapshot_data()) != nullptr in the failed linux-x64 / nn-test / Run Node.js Tests job. The fix should either exclude ELECTRON_RUN_AS_NODE from NodeSnapshotForThisProcess() or plumb the same snapshot data through node_main.cc.
| + { | ||
| + static std::string blob_bytes; // must outlive the SnapshotCreator | ||
| + static v8::StartupData base_blob{nullptr, 0}; | ||
| + std::vector<char*> filtered; | ||
| + const std::string kFlag = "--electron-v8-snapshot-blob="; | ||
| + for (int i = 0; i < argc; ++i) { | ||
| + if (std::string_view(argv[i]).substr(0, kFlag.size()) == kFlag) { | ||
| + const char* path = argv[i] + kFlag.size(); | ||
| + std::ifstream in(path, std::ios::binary); | ||
| + std::stringstream ss; | ||
| + ss << in.rdbuf(); | ||
| + blob_bytes = ss.str(); | ||
| + base_blob.data = blob_bytes.data(); | ||
| + base_blob.raw_size = static_cast<int>(blob_bytes.size()); | ||
| + node::SnapshotBuilder::SetBaseSnapshotForCreation(&base_blob); | ||
| + } else { | ||
| + filtered.push_back(argv[i]); | ||
| + } | ||
| + } | ||
| + if (static_cast<int>(filtered.size()) != argc) { | ||
| + argc = static_cast<int>(filtered.size()); | ||
| + for (int i = 0; i < argc; ++i) | ||
| + argv[i] = filtered[i]; | ||
| + argv[argc] = nullptr; | ||
| + } | ||
| + } |
There was a problem hiding this comment.
Node.js has node::ReadFileSync() that should more efficient here than a std::stringstream
static std::string blob_bytes; // must outlive the SnapshotCreator
static v8::StartupData base_blob{nullptr, 0};
static constexpr std::string_view kBlobFlag = "--electron-v8-snapshot-blob=";
for (int i = 0; i < argc; ++i) {
const std::string_view arg(argv[i]);
if (!arg.starts_with(kBlobFlag))
continue;
const char* path = argv[i] + kBlobFlag.size();
const int r = node::ReadFileSync(&blob_bytes, path);
if (r != 0) {
std::cerr << "Cannot read V8 snapshot blob " << path << ": "
<< uv_strerror(r) << "\n";
return 1;
}
base_blob.data = blob_bytes.data();
base_blob.raw_size = static_cast<int>(blob_bytes.size());
node::SnapshotBuilder::SetBaseSnapshotForCreation(&base_blob);
for (; i + 1 < argc; ++i)
argv[i] = argv[i + 1];
argv[--argc] = nullptr;
break;
}There was a problem hiding this comment.
Done in a372db9 — switched to node::ReadFileSync, which also reports a proper error (uv_strerror) on a missing/unreadable path instead of silently building from an empty blob. Dropped the now-unused <fstream>/<sstream> includes too.
| - auto result = code_cache_->map.emplace(id, data); | ||
| - USE(result.second); | ||
| - DCHECK(result.second); | ||
| + code_cache_->map.insert_or_assign(id, data); |
There was a problem hiding this comment.
I'm not sure I'm understanding this right. From the code comment, it sounds like we want this to be additive but not to overwrite pre-existing entries, but it adds new code that allows overwrites.
If we don't want to allow replacement, then the previous emplace + DCHECK code was better.
If we do want to allow replacement, the code comment should be rewritten
There was a problem hiding this comment.
Good catch — the comment was wrong. I instrumented RefreshCodeCache and confirmed the id sets do overlap: node_mksnapshot compiles the electron/js2c/* bundles (browser_init, node_init) while building the snapshot, so the snapshot embeds caches for them, and the build-time js2c cache then covers those same ids. So the replacement is intentional — insert_or_assign lets the later (build-time) entry win, and both were built against this isolate so either is valid. I've rewritten the comment to describe that, and fixed the same incorrect "snapshot doesn't cover these" claim in LoadEnvironment. a372db9
e83f6ca to
fbe134e
Compare
|
Re: the |
Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot.
The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before.
The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition.
When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect.
- node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment.
a372db9 to
8710267
Compare
| +// makes snapshot creation abort at SerializedHandleChecker / encoder. | ||
| +// Drop c_function when building a snapshot; the deserialized template uses | ||
| +// the (correct, just slower) slow callback. node_mksnapshot.cc sets the env | ||
| +// var; it is never set in shipped processes. |
There was a problem hiding this comment.
This will have a runtime cost to the fast paths node exercises right ?
Looking at https://chromium-review.googlesource.com/c/v8/v8/+/7668829 the managed pointer is an indirection at the end of the day, could we teach the snapshot (de)serializer to extract/rebuild c_function static address and signature from this object.
cc @joyeecheung this will affect Node.js.
There was a problem hiding this comment.
oh wait this is very likely addressed with https://chromium-review.googlesource.com/c/v8/v8/+/7828135, @MarshallOfSound can you pull in the change and see if the patch can be dropped.
There was a problem hiding this comment.
Confirmed it's in our pinned V8 (the 150.0.7849.0 roll) and pulled it in to try dropping the patch. It gets us most of the way but surfaced a second requirement: that CL stores the v8::CFunction object in a Foreign<kCFunctionTag>, so the snapshot serializer now encodes the CFunction object address itself — and ExternalReferenceRegistry::Register(const v8::CFunction&) only registers GetAddress() (the callback) + GetTypeInfo(), not &c_func. So node_mksnapshot aborted with Unknown external reference 0x… on the first fast-API template.
Fix is a one-liner — also register &c_func:
void Register(const v8::CFunction& c_func) {
RegisterT(&c_func); // CFunction object, now serialized via Foreign<kCFunctionTag>
RegisterT(c_func.GetAddress());
RegisterT(c_func.GetTypeInfo());
}With that, node_mksnapshot serializes the fast-API FunctionTemplates with c_function intact, so I've dropped DropCFunctionForSnapshot entirely — the browser process keeps the V8 fast paths (no more slow-callback fallback), and the patch is net ~75 lines smaller. Pushed in 2f9715a.
This is really an upstream Node gap (its registry will need the same &c_func once Node picks up this V8) — cc @joyeecheung. Happy to send it upstream; Electron carries it in the snapshot patch meanwhile.
deepak1556
left a comment
There was a problem hiding this comment.
Needs a followup on the fast api patch, LGTM otherwise.
V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in
FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction
in a Foreign instead of a Managed<>, which restores serialization of
FunctionTemplateInfo. The snapshot serializer now encodes the CFunction
object's address, so ExternalReferenceRegistry::Register(CFunction) must
register &c_func alongside GetAddress()/GetTypeInfo().
With that, node_mksnapshot serializes fast-API FunctionTemplates with
c_function intact, so the DropCFunctionForSnapshot workaround -- which
forced the snapshot's templates onto their slow callbacks -- is removed.
The browser process keeps the V8 fast-API paths.
|
Thanks for the ping, this fix mentioned in #51703 (comment) is similar to v8/node#254 (note that if you enable wasi in Node.js it will need the fix in wasi in that patch too), we have been floating it in the canary-base branch of Node.js, pending to go into the main branch as part of the V8 14.8 upgrade nodejs/node#62572 |
|
Release Notes Persisted
|
|
@MarshallOfSound has manually backported this PR to "43-x-y", please check out #51792 |
43-x-y is on V8 15.0.43, which stores a FunctionTemplateInfo's fast-API CFunction in a non-serializable Managed<> object. The browser-process Node startup snapshot (#51703) therefore aborts at build time in node_mksnapshot with "CheckGlobalAndEternalHandles failed" on unserialized <Foreign> handles. Backport upstream V8 CL 84f4af52c65 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo"), which wraps the CFunction in a Foreign stored directly in FunctionTemplateInfo and restores serializability. The node patch's external-reference registration for the CFunction address only matches V8 once this CL is present. Drop this patch when V8 is rolled past the upstream CL.
…#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291)
…#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291)
…1697, #51703) (#51792) * perf: push sandboxed renderer startup data via mojo and cache preload bytecode (#51602) * perf: push sandboxed renderer startup data via mojo instead of sync IPC Replaces the BROWSER_SANDBOX_LOAD sync IPC pull with a mojo push (ElectronFrameStartup.SetStartupData) sent from the browser at ReadyToCommitNavigation, ahead of CommitNavigation. The push carries preload script contents (as BigBuffer, shared-memory backed for large bundles), the browser's environment snapshot, and process.helperExecPath. The sync IPC parked the renderer main thread on a browser UI-thread roundtrip during every navigation. Its handler was async with await fs.promises.readFile() per preload, so under UI-thread contention each await yielded a turn to other startup work and the stall amplified ~5-8x with two preloads — measured at ~97ms with 20ms busy chunks. The push eliminates the roundtrip entirely; the renderer reads the data from a per-frame cache that is guaranteed to be populated before DidCreateScriptContext fires, because the message is associated with the navigation channel and ordered before CommitNavigation. The legacy sync IPC remains as a fallback for the cases the push does not yet cover (devtools extension subframes, service worker preload realms). process.arch/platform/version/versions are no longer shipped over the wire — they are identical in the renderer (same Electron binary) and are populated from node::per_process::metadata locally. * perf: remove BROWSER_SANDBOX_LOAD sync IPC entirely The previous commit kept the sync IPC as a fallback for paths that don't get a per-frame ElectronFrameStartup push: service worker preload realms (no RenderFrame, worker thread) and window.open() child windows whose synchronous about:blank document is created before the browser knows the window exists. Both now have a dedicated push and the IPC is removed: Service workers — a per-process associated mojom::ElectronWorkerStartup interface is registered via a RenderThreadObserver and the browser pushes from RenderProcessHostObserver::RenderProcessReady(), strictly before any StartWorker can spawn a worker thread. Cached behind a lock + WaitableEvent so the worker thread can block on it. Re-pushed when SW preload scripts are registered or unregistered while a renderer is alive. window.open() child windows — the popup's RenderFrame, its synchronous about:blank document, and its preload all happen inside the renderer's window.open() call stack, after the [Sync] CreateNewWindow IPC returns and before control returns to page script. No async browser-to-renderer mojo message can land in time. The browser does, however, know everything about the popup during CreateNewWindow (it just ran setWindowOpenHandler and may have overridden the preload, partition, etc.), so the only correct zero-extra-IPC transport is to attach the data to the CreateNewWindowReply itself. A new Chromium patch adds an opaque mojo_base.BigBuffer? field to CreateNewWindowReply, plus ContentBrowserClient::GetExtraCreateNewWindowReplyData() and ContentRendererClient::SetPendingCreateNewWindowStartupData() hooks; the renderer stashes the deserialized data process-globally for the next ElectronApiServiceImpl constructor on the same call stack. The browser-side data is built by a new renderer_startup_data::Build() helper shared by the navigation push, the per-process SW push, and the CreateNewWindowReply path. process.env is captured fresh per push via uv_os_environ() — same per-navigation timing as the legacy { ...process.env } spread. process.arch/platform/version/versions are filled from node::per_process::metadata in the renderer (same Electron binary) instead of being shipped over the wire; cldr/tz are runtime ICU state and computed directly from the renderer's ICU. BROWSER_NONSANDBOX_LOAD remains for non-sandboxed renderers — it returns preload paths only (no contents, no env spread), so the contention amplification doesn't apply. Verified against stock Electron 42.0.1 that window.open() popups with setWindowOpenHandler-overridden preloads behave identically. * perf: V8 code cache for sandboxed preload scripts Adds an in-memory + on-disk V8 code cache for preload scripts so the parse + top-level codegen cost is paid once per preload per Electron version instead of per navigation, and moves preload script compilation fully into native code so the contents and cache never bounce through the V8 heap. createPreloadScript() now takes (scriptId, paramNames) instead of a wrapped source string. It looks up the preload contents and code cache directly from the per-frame ElectronApiServiceImpl's mojo-cached startup data — the contents stay in the BigBuffer until a single NewFromUtf8() copy for the compile (rather than BigBuffer → V8 string → template-literal concat → flatten, ~150 KB of allocation and 2-3 copies per preload eliminated), and the cache bytes never enter V8 at all (BufferNotOwned for consume, direct BigBuffer construction from the produced CachedData for ship-back over ElectronWebContentsUtility.SetPreloadCodeCache). BuildStartupData() exposes hasContents instead of contents to JS. The compile uses ScriptCompiler::CompileFunction() with the parameter names directly instead of a string-templated (function(p0, p1, ...){…}) wrapper — no concat, no flatten, and preload stack traces get correct line numbers (the wrapper offsets every line by 1 with no ScriptOrigin::lineOffset compensation). The browser keys the cache by sha256(id) and persists it to userData/Code Cache/electron-preload/. V8's CachedData validation handles invalidation: a blob from a different V8 version, flag set, or source is rejected at consume time, the renderer compiles from source and ships a fresh blob, and the stale one is overwritten. Producing the cache during the cold compile adds ~25% to the first navigation per launch; warm navigations skip the parse (~5 ms saved per nav in a debug build, ~22% reduction in pre-parse blocking time). Service-worker preload realms look up contents from a clone of the worker startup data captured on ServiceWorkerData when the preload realm is created. They have no per-frame ship-back channel so they consume but never produce caches; the experimental --service-worker-preload flow is otherwise unchanged. * test: add spec coverage for preload startup data push and code cache - preload code cache: produces and persists a blob to userData/Code Cache/electron-preload/, runs the preload identically when consuming the cache, rejects and re-produces a corrupt disk cache (V8 CachedData validation rejects garbage and version-mismatched blobs and the renderer self-heals). - preload script stack traces: errors report correct line numbers and the preload's real file path instead of <anonymous>, sandboxed and not. CompileFunction() with an explicit ScriptOrigin gets both right; the legacy template-string wrapper had neither. - service worker preload realm: process.env, process.execPath, process.arch/platform/version, and process.versions.electron/chrome/node are populated from the per-process ElectronWorkerStartup push. - window.open() popup preload: the popup's synchronous about:blank document runs the preload from setWindowOpenHandler's overrideBrowserWindowOptions when set (delivered via CreateNewWindowReply), no preload when only security prefs are inherited (matching legacy Electron), and the opener's preload when explicitly carried through the override. * Delete .claude/scheduled_tasks.lock * refactor: address PR review feedback Review fixups independent of the service-worker delivery change: - preload_code_cache: DCHECK Get/Set are UI-thread only; SKIP_ON_SHUTDOWN for the disk write so an in-flight write isn't abandoned mid-write. Did NOT take the suggested first-writer-wins guard — Get() memoizes a corrupt on-disk blob (only V8 can validate it), so a guard keying on 'entry exists' would pin a bad cache and break the self-heal path; the duplicate-ship case is rare, identical bytes, and Set() is UI-thread serialized so it isn't a data race. - WebContentsPreferences::ShouldUseSandbox(): extract the duplicated 'null prefs => default sandboxed, --enable-sandbox forces sandbox' check used by the per-frame push. - preload_utils: LookupPreloadScript returns const PreloadScriptData* not const unique_ptr<>*; param name V8 conversion moved below the null-check so a missing script bails without paying for it. - electron_api_service_impl: drop unused TakeStartupData(); DCHECK the window.open pending-data slot is touched only on the renderer main thread; tighten the stale startup_data() comment. - Stop assigning the unused PreloadScriptData.type field (the field itself is removed in the following commit with its last setters). * perf: deliver service worker preload data via EmbeddedWorkerStartParams Replaces the per-process ElectronWorkerStartup push + the renderer-side cross-thread global (ElectronRenderThreadObserver: a process-global WaitableEvent + lock the SW worker thread could block on) with delivery through the service worker's own EmbeddedWorkerStartParams. A new ContentBrowserClient::GetServiceWorkerStartupData() hook populates an opaque BigBuffer on the start params; it rides the marshalling StartWorker already performs (GlobalScopeCreationParams moved onto the worker thread by WorkerThread::Start), so the data arrives on the worker thread ordered with worker creation — no global, no lock, no blocking wait, and no 'worker raced the push' edge case to handle. The renderer reads it off WebServiceWorkerContextProxy::ElectronPreloadData() (backed by WorkerGlobalScope) when it builds the preload realm. GetServiceWorkerStartupData() early-returns when the session has no service-worker preloads, so the asar reads + serialization no longer run on every renderer at RenderProcessReady() (addresses the SendToProcess cost raised in review). Registry changes are picked up automatically — the data is rebuilt from SessionPreferences on every StartWorker — so Session::BroadcastWorkerStartupData and its re-push on register/ unregister are removed. Also folds, since they share these files: removal of the now-unused PreloadScriptData.type field and its last setters, the CreateNewWindowReply target_url gate so the embedder blob isn't built for popups that navigate (review feedback), and the corrected CreateNewWindowReply patch commit message. (cherry picked from commit 07f25ea) * 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 (cherry picked from commit d857117) * perf: boot the browser process from an embedded Node startup snapshot (#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291) * build: regenerate filenames.auto.gni for backported bundle change The renderer startup-data backport (#51602) drops the dependency on lib/renderer/ipc-renderer-internal-utils.ts from the sandboxed renderer bundle. Regenerate filenames.auto.gni so the gen-filenames --check CI step passes. * chore: update patches * chore: update patches --------- Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
|
@MarshallOfSound has manually backported this PR to "42-x-y", please check out #51831 |
…1697, #51703) (#51792) * perf: push sandboxed renderer startup data via mojo and cache preload bytecode (#51602) * perf: push sandboxed renderer startup data via mojo instead of sync IPC Replaces the BROWSER_SANDBOX_LOAD sync IPC pull with a mojo push (ElectronFrameStartup.SetStartupData) sent from the browser at ReadyToCommitNavigation, ahead of CommitNavigation. The push carries preload script contents (as BigBuffer, shared-memory backed for large bundles), the browser's environment snapshot, and process.helperExecPath. The sync IPC parked the renderer main thread on a browser UI-thread roundtrip during every navigation. Its handler was async with await fs.promises.readFile() per preload, so under UI-thread contention each await yielded a turn to other startup work and the stall amplified ~5-8x with two preloads — measured at ~97ms with 20ms busy chunks. The push eliminates the roundtrip entirely; the renderer reads the data from a per-frame cache that is guaranteed to be populated before DidCreateScriptContext fires, because the message is associated with the navigation channel and ordered before CommitNavigation. The legacy sync IPC remains as a fallback for the cases the push does not yet cover (devtools extension subframes, service worker preload realms). process.arch/platform/version/versions are no longer shipped over the wire — they are identical in the renderer (same Electron binary) and are populated from node::per_process::metadata locally. * perf: remove BROWSER_SANDBOX_LOAD sync IPC entirely The previous commit kept the sync IPC as a fallback for paths that don't get a per-frame ElectronFrameStartup push: service worker preload realms (no RenderFrame, worker thread) and window.open() child windows whose synchronous about:blank document is created before the browser knows the window exists. Both now have a dedicated push and the IPC is removed: Service workers — a per-process associated mojom::ElectronWorkerStartup interface is registered via a RenderThreadObserver and the browser pushes from RenderProcessHostObserver::RenderProcessReady(), strictly before any StartWorker can spawn a worker thread. Cached behind a lock + WaitableEvent so the worker thread can block on it. Re-pushed when SW preload scripts are registered or unregistered while a renderer is alive. window.open() child windows — the popup's RenderFrame, its synchronous about:blank document, and its preload all happen inside the renderer's window.open() call stack, after the [Sync] CreateNewWindow IPC returns and before control returns to page script. No async browser-to-renderer mojo message can land in time. The browser does, however, know everything about the popup during CreateNewWindow (it just ran setWindowOpenHandler and may have overridden the preload, partition, etc.), so the only correct zero-extra-IPC transport is to attach the data to the CreateNewWindowReply itself. A new Chromium patch adds an opaque mojo_base.BigBuffer? field to CreateNewWindowReply, plus ContentBrowserClient::GetExtraCreateNewWindowReplyData() and ContentRendererClient::SetPendingCreateNewWindowStartupData() hooks; the renderer stashes the deserialized data process-globally for the next ElectronApiServiceImpl constructor on the same call stack. The browser-side data is built by a new renderer_startup_data::Build() helper shared by the navigation push, the per-process SW push, and the CreateNewWindowReply path. process.env is captured fresh per push via uv_os_environ() — same per-navigation timing as the legacy { ...process.env } spread. process.arch/platform/version/versions are filled from node::per_process::metadata in the renderer (same Electron binary) instead of being shipped over the wire; cldr/tz are runtime ICU state and computed directly from the renderer's ICU. BROWSER_NONSANDBOX_LOAD remains for non-sandboxed renderers — it returns preload paths only (no contents, no env spread), so the contention amplification doesn't apply. Verified against stock Electron 42.0.1 that window.open() popups with setWindowOpenHandler-overridden preloads behave identically. * perf: V8 code cache for sandboxed preload scripts Adds an in-memory + on-disk V8 code cache for preload scripts so the parse + top-level codegen cost is paid once per preload per Electron version instead of per navigation, and moves preload script compilation fully into native code so the contents and cache never bounce through the V8 heap. createPreloadScript() now takes (scriptId, paramNames) instead of a wrapped source string. It looks up the preload contents and code cache directly from the per-frame ElectronApiServiceImpl's mojo-cached startup data — the contents stay in the BigBuffer until a single NewFromUtf8() copy for the compile (rather than BigBuffer → V8 string → template-literal concat → flatten, ~150 KB of allocation and 2-3 copies per preload eliminated), and the cache bytes never enter V8 at all (BufferNotOwned for consume, direct BigBuffer construction from the produced CachedData for ship-back over ElectronWebContentsUtility.SetPreloadCodeCache). BuildStartupData() exposes hasContents instead of contents to JS. The compile uses ScriptCompiler::CompileFunction() with the parameter names directly instead of a string-templated (function(p0, p1, ...){…}) wrapper — no concat, no flatten, and preload stack traces get correct line numbers (the wrapper offsets every line by 1 with no ScriptOrigin::lineOffset compensation). The browser keys the cache by sha256(id) and persists it to userData/Code Cache/electron-preload/. V8's CachedData validation handles invalidation: a blob from a different V8 version, flag set, or source is rejected at consume time, the renderer compiles from source and ships a fresh blob, and the stale one is overwritten. Producing the cache during the cold compile adds ~25% to the first navigation per launch; warm navigations skip the parse (~5 ms saved per nav in a debug build, ~22% reduction in pre-parse blocking time). Service-worker preload realms look up contents from a clone of the worker startup data captured on ServiceWorkerData when the preload realm is created. They have no per-frame ship-back channel so they consume but never produce caches; the experimental --service-worker-preload flow is otherwise unchanged. * test: add spec coverage for preload startup data push and code cache - preload code cache: produces and persists a blob to userData/Code Cache/electron-preload/, runs the preload identically when consuming the cache, rejects and re-produces a corrupt disk cache (V8 CachedData validation rejects garbage and version-mismatched blobs and the renderer self-heals). - preload script stack traces: errors report correct line numbers and the preload's real file path instead of <anonymous>, sandboxed and not. CompileFunction() with an explicit ScriptOrigin gets both right; the legacy template-string wrapper had neither. - service worker preload realm: process.env, process.execPath, process.arch/platform/version, and process.versions.electron/chrome/node are populated from the per-process ElectronWorkerStartup push. - window.open() popup preload: the popup's synchronous about:blank document runs the preload from setWindowOpenHandler's overrideBrowserWindowOptions when set (delivered via CreateNewWindowReply), no preload when only security prefs are inherited (matching legacy Electron), and the opener's preload when explicitly carried through the override. * Delete .claude/scheduled_tasks.lock * refactor: address PR review feedback Review fixups independent of the service-worker delivery change: - preload_code_cache: DCHECK Get/Set are UI-thread only; SKIP_ON_SHUTDOWN for the disk write so an in-flight write isn't abandoned mid-write. Did NOT take the suggested first-writer-wins guard — Get() memoizes a corrupt on-disk blob (only V8 can validate it), so a guard keying on 'entry exists' would pin a bad cache and break the self-heal path; the duplicate-ship case is rare, identical bytes, and Set() is UI-thread serialized so it isn't a data race. - WebContentsPreferences::ShouldUseSandbox(): extract the duplicated 'null prefs => default sandboxed, --enable-sandbox forces sandbox' check used by the per-frame push. - preload_utils: LookupPreloadScript returns const PreloadScriptData* not const unique_ptr<>*; param name V8 conversion moved below the null-check so a missing script bails without paying for it. - electron_api_service_impl: drop unused TakeStartupData(); DCHECK the window.open pending-data slot is touched only on the renderer main thread; tighten the stale startup_data() comment. - Stop assigning the unused PreloadScriptData.type field (the field itself is removed in the following commit with its last setters). * perf: deliver service worker preload data via EmbeddedWorkerStartParams Replaces the per-process ElectronWorkerStartup push + the renderer-side cross-thread global (ElectronRenderThreadObserver: a process-global WaitableEvent + lock the SW worker thread could block on) with delivery through the service worker's own EmbeddedWorkerStartParams. A new ContentBrowserClient::GetServiceWorkerStartupData() hook populates an opaque BigBuffer on the start params; it rides the marshalling StartWorker already performs (GlobalScopeCreationParams moved onto the worker thread by WorkerThread::Start), so the data arrives on the worker thread ordered with worker creation — no global, no lock, no blocking wait, and no 'worker raced the push' edge case to handle. The renderer reads it off WebServiceWorkerContextProxy::ElectronPreloadData() (backed by WorkerGlobalScope) when it builds the preload realm. GetServiceWorkerStartupData() early-returns when the session has no service-worker preloads, so the asar reads + serialization no longer run on every renderer at RenderProcessReady() (addresses the SendToProcess cost raised in review). Registry changes are picked up automatically — the data is rebuilt from SessionPreferences on every StartWorker — so Session::BroadcastWorkerStartupData and its re-push on register/ unregister are removed. Also folds, since they share these files: removal of the now-unused PreloadScriptData.type field and its last setters, the CreateNewWindowReply target_url gate so the embedder blob isn't built for popups that navigate (review feedback), and the corrected CreateNewWindowReply patch commit message. (cherry picked from commit 07f25ea) * 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 (cherry picked from commit d857117) * perf: boot the browser process from an embedded Node startup snapshot (#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291) * build: regenerate filenames.auto.gni for backported bundle change The renderer startup-data backport (#51602) drops the dependency on lib/renderer/ipc-renderer-internal-utils.ts from the sandboxed renderer bundle. Regenerate filenames.auto.gni so the gen-filenames --check CI step passes. * chore: update patches * chore: update patches --------- Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com> (cherry picked from commit ea8b0bb)
…1697, #51703) (#51792) * perf: push sandboxed renderer startup data via mojo and cache preload bytecode (#51602) * perf: push sandboxed renderer startup data via mojo instead of sync IPC Replaces the BROWSER_SANDBOX_LOAD sync IPC pull with a mojo push (ElectronFrameStartup.SetStartupData) sent from the browser at ReadyToCommitNavigation, ahead of CommitNavigation. The push carries preload script contents (as BigBuffer, shared-memory backed for large bundles), the browser's environment snapshot, and process.helperExecPath. The sync IPC parked the renderer main thread on a browser UI-thread roundtrip during every navigation. Its handler was async with await fs.promises.readFile() per preload, so under UI-thread contention each await yielded a turn to other startup work and the stall amplified ~5-8x with two preloads — measured at ~97ms with 20ms busy chunks. The push eliminates the roundtrip entirely; the renderer reads the data from a per-frame cache that is guaranteed to be populated before DidCreateScriptContext fires, because the message is associated with the navigation channel and ordered before CommitNavigation. The legacy sync IPC remains as a fallback for the cases the push does not yet cover (devtools extension subframes, service worker preload realms). process.arch/platform/version/versions are no longer shipped over the wire — they are identical in the renderer (same Electron binary) and are populated from node::per_process::metadata locally. * perf: remove BROWSER_SANDBOX_LOAD sync IPC entirely The previous commit kept the sync IPC as a fallback for paths that don't get a per-frame ElectronFrameStartup push: service worker preload realms (no RenderFrame, worker thread) and window.open() child windows whose synchronous about:blank document is created before the browser knows the window exists. Both now have a dedicated push and the IPC is removed: Service workers — a per-process associated mojom::ElectronWorkerStartup interface is registered via a RenderThreadObserver and the browser pushes from RenderProcessHostObserver::RenderProcessReady(), strictly before any StartWorker can spawn a worker thread. Cached behind a lock + WaitableEvent so the worker thread can block on it. Re-pushed when SW preload scripts are registered or unregistered while a renderer is alive. window.open() child windows — the popup's RenderFrame, its synchronous about:blank document, and its preload all happen inside the renderer's window.open() call stack, after the [Sync] CreateNewWindow IPC returns and before control returns to page script. No async browser-to-renderer mojo message can land in time. The browser does, however, know everything about the popup during CreateNewWindow (it just ran setWindowOpenHandler and may have overridden the preload, partition, etc.), so the only correct zero-extra-IPC transport is to attach the data to the CreateNewWindowReply itself. A new Chromium patch adds an opaque mojo_base.BigBuffer? field to CreateNewWindowReply, plus ContentBrowserClient::GetExtraCreateNewWindowReplyData() and ContentRendererClient::SetPendingCreateNewWindowStartupData() hooks; the renderer stashes the deserialized data process-globally for the next ElectronApiServiceImpl constructor on the same call stack. The browser-side data is built by a new renderer_startup_data::Build() helper shared by the navigation push, the per-process SW push, and the CreateNewWindowReply path. process.env is captured fresh per push via uv_os_environ() — same per-navigation timing as the legacy { ...process.env } spread. process.arch/platform/version/versions are filled from node::per_process::metadata in the renderer (same Electron binary) instead of being shipped over the wire; cldr/tz are runtime ICU state and computed directly from the renderer's ICU. BROWSER_NONSANDBOX_LOAD remains for non-sandboxed renderers — it returns preload paths only (no contents, no env spread), so the contention amplification doesn't apply. Verified against stock Electron 42.0.1 that window.open() popups with setWindowOpenHandler-overridden preloads behave identically. * perf: V8 code cache for sandboxed preload scripts Adds an in-memory + on-disk V8 code cache for preload scripts so the parse + top-level codegen cost is paid once per preload per Electron version instead of per navigation, and moves preload script compilation fully into native code so the contents and cache never bounce through the V8 heap. createPreloadScript() now takes (scriptId, paramNames) instead of a wrapped source string. It looks up the preload contents and code cache directly from the per-frame ElectronApiServiceImpl's mojo-cached startup data — the contents stay in the BigBuffer until a single NewFromUtf8() copy for the compile (rather than BigBuffer → V8 string → template-literal concat → flatten, ~150 KB of allocation and 2-3 copies per preload eliminated), and the cache bytes never enter V8 at all (BufferNotOwned for consume, direct BigBuffer construction from the produced CachedData for ship-back over ElectronWebContentsUtility.SetPreloadCodeCache). BuildStartupData() exposes hasContents instead of contents to JS. The compile uses ScriptCompiler::CompileFunction() with the parameter names directly instead of a string-templated (function(p0, p1, ...){…}) wrapper — no concat, no flatten, and preload stack traces get correct line numbers (the wrapper offsets every line by 1 with no ScriptOrigin::lineOffset compensation). The browser keys the cache by sha256(id) and persists it to userData/Code Cache/electron-preload/. V8's CachedData validation handles invalidation: a blob from a different V8 version, flag set, or source is rejected at consume time, the renderer compiles from source and ships a fresh blob, and the stale one is overwritten. Producing the cache during the cold compile adds ~25% to the first navigation per launch; warm navigations skip the parse (~5 ms saved per nav in a debug build, ~22% reduction in pre-parse blocking time). Service-worker preload realms look up contents from a clone of the worker startup data captured on ServiceWorkerData when the preload realm is created. They have no per-frame ship-back channel so they consume but never produce caches; the experimental --service-worker-preload flow is otherwise unchanged. * test: add spec coverage for preload startup data push and code cache - preload code cache: produces and persists a blob to userData/Code Cache/electron-preload/, runs the preload identically when consuming the cache, rejects and re-produces a corrupt disk cache (V8 CachedData validation rejects garbage and version-mismatched blobs and the renderer self-heals). - preload script stack traces: errors report correct line numbers and the preload's real file path instead of <anonymous>, sandboxed and not. CompileFunction() with an explicit ScriptOrigin gets both right; the legacy template-string wrapper had neither. - service worker preload realm: process.env, process.execPath, process.arch/platform/version, and process.versions.electron/chrome/node are populated from the per-process ElectronWorkerStartup push. - window.open() popup preload: the popup's synchronous about:blank document runs the preload from setWindowOpenHandler's overrideBrowserWindowOptions when set (delivered via CreateNewWindowReply), no preload when only security prefs are inherited (matching legacy Electron), and the opener's preload when explicitly carried through the override. * Delete .claude/scheduled_tasks.lock * refactor: address PR review feedback Review fixups independent of the service-worker delivery change: - preload_code_cache: DCHECK Get/Set are UI-thread only; SKIP_ON_SHUTDOWN for the disk write so an in-flight write isn't abandoned mid-write. Did NOT take the suggested first-writer-wins guard — Get() memoizes a corrupt on-disk blob (only V8 can validate it), so a guard keying on 'entry exists' would pin a bad cache and break the self-heal path; the duplicate-ship case is rare, identical bytes, and Set() is UI-thread serialized so it isn't a data race. - WebContentsPreferences::ShouldUseSandbox(): extract the duplicated 'null prefs => default sandboxed, --enable-sandbox forces sandbox' check used by the per-frame push. - preload_utils: LookupPreloadScript returns const PreloadScriptData* not const unique_ptr<>*; param name V8 conversion moved below the null-check so a missing script bails without paying for it. - electron_api_service_impl: drop unused TakeStartupData(); DCHECK the window.open pending-data slot is touched only on the renderer main thread; tighten the stale startup_data() comment. - Stop assigning the unused PreloadScriptData.type field (the field itself is removed in the following commit with its last setters). * perf: deliver service worker preload data via EmbeddedWorkerStartParams Replaces the per-process ElectronWorkerStartup push + the renderer-side cross-thread global (ElectronRenderThreadObserver: a process-global WaitableEvent + lock the SW worker thread could block on) with delivery through the service worker's own EmbeddedWorkerStartParams. A new ContentBrowserClient::GetServiceWorkerStartupData() hook populates an opaque BigBuffer on the start params; it rides the marshalling StartWorker already performs (GlobalScopeCreationParams moved onto the worker thread by WorkerThread::Start), so the data arrives on the worker thread ordered with worker creation — no global, no lock, no blocking wait, and no 'worker raced the push' edge case to handle. The renderer reads it off WebServiceWorkerContextProxy::ElectronPreloadData() (backed by WorkerGlobalScope) when it builds the preload realm. GetServiceWorkerStartupData() early-returns when the session has no service-worker preloads, so the asar reads + serialization no longer run on every renderer at RenderProcessReady() (addresses the SendToProcess cost raised in review). Registry changes are picked up automatically — the data is rebuilt from SessionPreferences on every StartWorker — so Session::BroadcastWorkerStartupData and its re-push on register/ unregister are removed. Also folds, since they share these files: removal of the now-unused PreloadScriptData.type field and its last setters, the CreateNewWindowReply target_url gate so the embedder blob isn't built for popups that navigate (review feedback), and the corrected CreateNewWindowReply patch commit message. (cherry picked from commit 07f25ea) * 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 (cherry picked from commit d857117) * perf: boot the browser process from an embedded Node startup snapshot (#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291) * build: regenerate filenames.auto.gni for backported bundle change The renderer startup-data backport (#51602) drops the dependency on lib/renderer/ipc-renderer-internal-utils.ts from the sandboxed renderer bundle. Regenerate filenames.auto.gni so the gen-filenames --check CI step passes. * chore: update patches * chore: update patches --------- Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com> (cherry picked from commit ea8b0bb)
…1697, #51703) (#51831) * perf: backport startup performance improvements to 43-x-y (#51602, #51697, #51703) (#51792) * perf: push sandboxed renderer startup data via mojo and cache preload bytecode (#51602) * perf: push sandboxed renderer startup data via mojo instead of sync IPC Replaces the BROWSER_SANDBOX_LOAD sync IPC pull with a mojo push (ElectronFrameStartup.SetStartupData) sent from the browser at ReadyToCommitNavigation, ahead of CommitNavigation. The push carries preload script contents (as BigBuffer, shared-memory backed for large bundles), the browser's environment snapshot, and process.helperExecPath. The sync IPC parked the renderer main thread on a browser UI-thread roundtrip during every navigation. Its handler was async with await fs.promises.readFile() per preload, so under UI-thread contention each await yielded a turn to other startup work and the stall amplified ~5-8x with two preloads — measured at ~97ms with 20ms busy chunks. The push eliminates the roundtrip entirely; the renderer reads the data from a per-frame cache that is guaranteed to be populated before DidCreateScriptContext fires, because the message is associated with the navigation channel and ordered before CommitNavigation. The legacy sync IPC remains as a fallback for the cases the push does not yet cover (devtools extension subframes, service worker preload realms). process.arch/platform/version/versions are no longer shipped over the wire — they are identical in the renderer (same Electron binary) and are populated from node::per_process::metadata locally. * perf: remove BROWSER_SANDBOX_LOAD sync IPC entirely The previous commit kept the sync IPC as a fallback for paths that don't get a per-frame ElectronFrameStartup push: service worker preload realms (no RenderFrame, worker thread) and window.open() child windows whose synchronous about:blank document is created before the browser knows the window exists. Both now have a dedicated push and the IPC is removed: Service workers — a per-process associated mojom::ElectronWorkerStartup interface is registered via a RenderThreadObserver and the browser pushes from RenderProcessHostObserver::RenderProcessReady(), strictly before any StartWorker can spawn a worker thread. Cached behind a lock + WaitableEvent so the worker thread can block on it. Re-pushed when SW preload scripts are registered or unregistered while a renderer is alive. window.open() child windows — the popup's RenderFrame, its synchronous about:blank document, and its preload all happen inside the renderer's window.open() call stack, after the [Sync] CreateNewWindow IPC returns and before control returns to page script. No async browser-to-renderer mojo message can land in time. The browser does, however, know everything about the popup during CreateNewWindow (it just ran setWindowOpenHandler and may have overridden the preload, partition, etc.), so the only correct zero-extra-IPC transport is to attach the data to the CreateNewWindowReply itself. A new Chromium patch adds an opaque mojo_base.BigBuffer? field to CreateNewWindowReply, plus ContentBrowserClient::GetExtraCreateNewWindowReplyData() and ContentRendererClient::SetPendingCreateNewWindowStartupData() hooks; the renderer stashes the deserialized data process-globally for the next ElectronApiServiceImpl constructor on the same call stack. The browser-side data is built by a new renderer_startup_data::Build() helper shared by the navigation push, the per-process SW push, and the CreateNewWindowReply path. process.env is captured fresh per push via uv_os_environ() — same per-navigation timing as the legacy { ...process.env } spread. process.arch/platform/version/versions are filled from node::per_process::metadata in the renderer (same Electron binary) instead of being shipped over the wire; cldr/tz are runtime ICU state and computed directly from the renderer's ICU. BROWSER_NONSANDBOX_LOAD remains for non-sandboxed renderers — it returns preload paths only (no contents, no env spread), so the contention amplification doesn't apply. Verified against stock Electron 42.0.1 that window.open() popups with setWindowOpenHandler-overridden preloads behave identically. * perf: V8 code cache for sandboxed preload scripts Adds an in-memory + on-disk V8 code cache for preload scripts so the parse + top-level codegen cost is paid once per preload per Electron version instead of per navigation, and moves preload script compilation fully into native code so the contents and cache never bounce through the V8 heap. createPreloadScript() now takes (scriptId, paramNames) instead of a wrapped source string. It looks up the preload contents and code cache directly from the per-frame ElectronApiServiceImpl's mojo-cached startup data — the contents stay in the BigBuffer until a single NewFromUtf8() copy for the compile (rather than BigBuffer → V8 string → template-literal concat → flatten, ~150 KB of allocation and 2-3 copies per preload eliminated), and the cache bytes never enter V8 at all (BufferNotOwned for consume, direct BigBuffer construction from the produced CachedData for ship-back over ElectronWebContentsUtility.SetPreloadCodeCache). BuildStartupData() exposes hasContents instead of contents to JS. The compile uses ScriptCompiler::CompileFunction() with the parameter names directly instead of a string-templated (function(p0, p1, ...){…}) wrapper — no concat, no flatten, and preload stack traces get correct line numbers (the wrapper offsets every line by 1 with no ScriptOrigin::lineOffset compensation). The browser keys the cache by sha256(id) and persists it to userData/Code Cache/electron-preload/. V8's CachedData validation handles invalidation: a blob from a different V8 version, flag set, or source is rejected at consume time, the renderer compiles from source and ships a fresh blob, and the stale one is overwritten. Producing the cache during the cold compile adds ~25% to the first navigation per launch; warm navigations skip the parse (~5 ms saved per nav in a debug build, ~22% reduction in pre-parse blocking time). Service-worker preload realms look up contents from a clone of the worker startup data captured on ServiceWorkerData when the preload realm is created. They have no per-frame ship-back channel so they consume but never produce caches; the experimental --service-worker-preload flow is otherwise unchanged. * test: add spec coverage for preload startup data push and code cache - preload code cache: produces and persists a blob to userData/Code Cache/electron-preload/, runs the preload identically when consuming the cache, rejects and re-produces a corrupt disk cache (V8 CachedData validation rejects garbage and version-mismatched blobs and the renderer self-heals). - preload script stack traces: errors report correct line numbers and the preload's real file path instead of <anonymous>, sandboxed and not. CompileFunction() with an explicit ScriptOrigin gets both right; the legacy template-string wrapper had neither. - service worker preload realm: process.env, process.execPath, process.arch/platform/version, and process.versions.electron/chrome/node are populated from the per-process ElectronWorkerStartup push. - window.open() popup preload: the popup's synchronous about:blank document runs the preload from setWindowOpenHandler's overrideBrowserWindowOptions when set (delivered via CreateNewWindowReply), no preload when only security prefs are inherited (matching legacy Electron), and the opener's preload when explicitly carried through the override. * Delete .claude/scheduled_tasks.lock * refactor: address PR review feedback Review fixups independent of the service-worker delivery change: - preload_code_cache: DCHECK Get/Set are UI-thread only; SKIP_ON_SHUTDOWN for the disk write so an in-flight write isn't abandoned mid-write. Did NOT take the suggested first-writer-wins guard — Get() memoizes a corrupt on-disk blob (only V8 can validate it), so a guard keying on 'entry exists' would pin a bad cache and break the self-heal path; the duplicate-ship case is rare, identical bytes, and Set() is UI-thread serialized so it isn't a data race. - WebContentsPreferences::ShouldUseSandbox(): extract the duplicated 'null prefs => default sandboxed, --enable-sandbox forces sandbox' check used by the per-frame push. - preload_utils: LookupPreloadScript returns const PreloadScriptData* not const unique_ptr<>*; param name V8 conversion moved below the null-check so a missing script bails without paying for it. - electron_api_service_impl: drop unused TakeStartupData(); DCHECK the window.open pending-data slot is touched only on the renderer main thread; tighten the stale startup_data() comment. - Stop assigning the unused PreloadScriptData.type field (the field itself is removed in the following commit with its last setters). * perf: deliver service worker preload data via EmbeddedWorkerStartParams Replaces the per-process ElectronWorkerStartup push + the renderer-side cross-thread global (ElectronRenderThreadObserver: a process-global WaitableEvent + lock the SW worker thread could block on) with delivery through the service worker's own EmbeddedWorkerStartParams. A new ContentBrowserClient::GetServiceWorkerStartupData() hook populates an opaque BigBuffer on the start params; it rides the marshalling StartWorker already performs (GlobalScopeCreationParams moved onto the worker thread by WorkerThread::Start), so the data arrives on the worker thread ordered with worker creation — no global, no lock, no blocking wait, and no 'worker raced the push' edge case to handle. The renderer reads it off WebServiceWorkerContextProxy::ElectronPreloadData() (backed by WorkerGlobalScope) when it builds the preload realm. GetServiceWorkerStartupData() early-returns when the session has no service-worker preloads, so the asar reads + serialization no longer run on every renderer at RenderProcessReady() (addresses the SendToProcess cost raised in review). Registry changes are picked up automatically — the data is rebuilt from SessionPreferences on every StartWorker — so Session::BroadcastWorkerStartupData and its re-push on register/ unregister are removed. Also folds, since they share these files: removal of the now-unused PreloadScriptData.type field and its last setters, the CreateNewWindowReply target_url gate so the embedder blob isn't built for popups that navigate (review feedback), and the corrected CreateNewWindowReply patch commit message. (cherry picked from commit 07f25ea) * 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 (cherry picked from commit d857117) * perf: boot the browser process from an embedded Node startup snapshot (#51703) * perf: boot the browser process from an embedded Node startup snapshot Build a Node startup snapshot at build time (node_mksnapshot, gated to native builds) and create the browser-process isolate from it so the Node bootstrap is deserialized instead of re-parsed and re-compiled at every app start. The snapshot is generated by extending the same V8 startup snapshot the rest of the process uses (snapshot_blob.bin), so the read-only heap is shared and V8's one-snapshot-per-process invariant holds. node_snapshot (a separate source_set in third_party/electron_node:unofficial.gni) provides the strong SnapshotBuilder::GetEmbeddedSnapshotData over a now- weak no-op stub; electron_lib deps it so the shipped framework links the real snapshot while node_mksnapshot (which only links libnode + the weak stub) does not -- avoiding the GN dependency cycle that v8_snapshot_ toolchain == default_toolchain would otherwise force on native builds. Browser process only: - javascript_environment.cc: when the embedded snapshot is present, CreateIsolateHolder feeds its blob + external references into the isolate CreateParams (gin passes nullptr external refs so there is no conflict; the browser process has no blink). The ctor skips node::NewContext -- the main context is materialized from the snapshot inside node::CreateEnvironment. - electron_browser_main_parts.cc: passes an empty context to Initialize/CreateEnvironment in the snapshot path; enters the deserialized context after. - node_bindings.cc: CreateEnvironment defers context-dependent setup until the snapshot's main context exists; CreateIsolateData receives the snapshot's per-isolate data; LoadEnvironment merges the build-time js2c cache for electron/js2c/* with the snapshot's per-builtin cache. Renderer / sandboxed renderer / utility / worker processes are unchanged. Measured impact (Testing build, macOS arm64): V8 compile/parse on the browser-process critical path drops from ~17 ms to ~12 ms top-level (V8.PreParse 1003 -> 24 events, V8.ParseProgram 8 -> 0.7 ms), but the 4.4 MB snapshot adds ~5 ms of deserialize cost (V8.DeserializeContext 2.6 ms + V8.DeserializeIsolate 2.0 ms + V8.CompileDeserialize 1.4 ms), so the net wall-clock change is within run-to-run noise. The Testing build keeps every DCHECK in V8's deserializer hot loop compiled in (dcheck_always_on=true); the snapshot-vs-recompile trade-off is expected to be more favorable in Release builds where those are compiled out and the deserialize is mostly memcpy + relocate. Needs a Release benchmark to determine whether this is a net win. The build-time js2c code cache for the browser process is generated against the embedded snapshot's isolate (a dedicated electron_natives_codecache_snapshot host tool links the real node_snapshot and compiles browser_init/node_init via GetEmbeddedSnapshotData + InitializeIsolateParams). A SnapshotCreator- produced snapshot owns a unique read-only-heap checksum that matches no standalone blob, so this is the only way the browser bundles' code-cache read-only checksum matches the runtime isolate; the other flavors keep using the v8 context snapshot. * fix: restore preventDefault on native events in the snapshot path The eager gin_helper Event-constructor warm-up in NodeBindings::Initialize was guarded out when booting from the embedded Node startup snapshot (the context is empty there), on the mistaken assumption the Event ObjectTemplate would be populated lazily. For gin Constructible classes the template -- which carries preventDefault()/defaultPrevented -- is only ever populated by GetConstructor(), so every native gin event emitted in the snapshot-booted browser process was missing those members (e.g. TypeError: event.preventDefault is not a function on -before-unload-fired, will-navigate, etc.). Run the warm-up in CreateEnvironment once the snapshot's main context has been materialized and entered. The template is cached per-isolate, so a single call covers the isolate lifetime as before. * fix: skip the embedded snapshot in the ELECTRON_RUN_AS_NODE path The ELECTRON_RUN_AS_NODE entry point (shell/app/node_main.cc, used by child_process.fork) runs without a --type switch, so IsBrowserProcess() is true for it and JavascriptEnvironment created its isolate from the embedded Node startup snapshot. But node_main.cc builds its IsolateData without snapshot_data and passes a fresh context, so node's CreateEnvironment tripped CHECK_NOT_NULL(isolate_data->snapshot_data()) and the forked process aborted. Gate the snapshot on a new IsRunningAsNode() helper so only the genuine browser process consumes it; the run-as-node path boots a fresh isolate as before. The run-as-node check is lifted out of crash_keys.cc into process_util so both callers share one definition. * fix: avoid double uncaughtException delivery in the snapshot path When the browser process boots from the embedded Node snapshot, node::CreateEnvironment registers the per-isolate message listener while deserializing the snapshot (SetIsolateErrorHandlers in node's src/api/environment.cc, reached only on the snapshot path). Electron's own SetIsolateUpForNode then registered the same listener again, and because V8 message listeners are additive, every uncaught error -- and the uncaughtException that follows -- was delivered twice. A throwing webContents.setWindowOpenHandler surfaced this (guest-window-manager spec). Skip Electron's duplicate registration when booting from the snapshot: the snapshot-registered PerIsolateMessageListener is identical, and the custom fatal/OOM handlers are setters that still take effect. * chore: address review feedback on the snapshot patch - node_mksnapshot: read the base V8 snapshot blob with node::ReadFileSync instead of ifstream/stringstream, reporting a clear error on a missing or unreadable path rather than silently producing an empty blob. - Fix a misleading comment: the Node startup snapshot's embedded code cache and Electron's build-time js2c cache are NOT disjoint -- both carry the electron/js2c/* bundles node_mksnapshot compiled -- so RefreshCodeCache's insert_or_assign (build-time entry wins) is intentional, not a latent replace-vs-emplace bug. Correct the same claim in LoadEnvironment. * fix: register fast-API CFunction objects as snapshot external references V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", already in our pinned V8) wraps the v8::CFunction in a Foreign instead of a Managed<>, which restores serialization of FunctionTemplateInfo. The snapshot serializer now encodes the CFunction object's address, so ExternalReferenceRegistry::Register(CFunction) must register &c_func alongside GetAddress()/GetTypeInfo(). With that, node_mksnapshot serializes fast-API FunctionTemplates with c_function intact, so the DropCFunctionForSnapshot workaround -- which forced the snapshot's templates onto their slow callbacks -- is removed. The browser process keeps the V8 fast-API paths. (cherry picked from commit 8b55291) * build: regenerate filenames.auto.gni for backported bundle change The renderer startup-data backport (#51602) drops the dependency on lib/renderer/ipc-renderer-internal-utils.ts from the sandboxed renderer bundle. Regenerate filenames.auto.gni so the gen-filenames --check CI step passes. * chore: update patches * chore: update patches --------- Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com> (cherry picked from commit ea8b0bb) * build: backport V8 fastapi serialization fix for the Node startup snapshot 42-x-y pins Chromium 148 / V8 14.8, which stores fast-API CFunction overloads in FunctionTemplateInfo wrapped in Managed<> objects. Managed<> objects cannot be serialized, so node_mksnapshot (added by the startup snapshot backport) fails with "CheckGlobalAndEternalHandles failed" when it serializes fast-API FunctionTemplates. Backport upstream V8 CL 7828135 ("[fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo", first shipped in V8 15.0.163) as a V8 patch. It wraps the embedder's v8::CFunction in a Foreign instead, which restores serialization; the snapshot encodes the CFunction's address via the external references that node's ExternalReferenceRegistry registers. The only adaptation is keeping 14.8's GetCFunction(Isolate*, int) signature so existing wasm/logging callers are untouched. * build: prune stale hasown descriptor from yarn.lock The hasown@^2.0.0 descriptor was only requested by the get-intrinsic@1.2.4 lockfile entry, which the get-intrinsic resolutions pin removed. Yarn's hardened-mode validation in CI rejects the stale descriptor in the entry key. * chore: update patches --------- Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Description of Change
Builds a Node.js startup snapshot for the browser (main) process at compile time and creates the browser-process isolate from it, so the Node bootstrap is deserialized instead of re-parsed and re-compiled from source on every app start. Only the browser process is touched — renderer / sandboxed renderer / utility / worker are unchanged.
In a Release build this cuts time-to-first-main-process-JS by ~40% (123 → 74 ms) and total spawn →
app 'ready'by ~25% (198 → 149 ms), at a cost of +4.6 MB binary size.Stacks on #51697: the browser process consumes both the build-time
electron/js2ccode cache from that PR and this Node snapshot (LoadEnvironmentmerges the two).Benchmark detail
Release build (
is_official_build=true,dcheck_always_on=false), x64 Linux, 24 cold samples/build, measured from process spawn:bootstrapCompleteapp 'ready'(total)first-JS→ready(Chromium browser-init)The 4.4 MB snapshot adds ~4.6 ms of deserialize on the pre-
readypath (already netted in). Thefirst-JS → readysegment is Chromium browser-init, which this doesn't touch, so it's flat — the win is the Node/Electron bootstrap.How it works
The snapshot extends the same V8 startup snapshot the rest of the process already uses (
snapshot_blob.bin), so the read-only heap stays shared and V8's one-snapshot-per-process invariant holds.node_snapshot(a separatesource_setinthird_party/electron_node:unofficial.gni) provides the strongSnapshotBuilder::GetEmbeddedSnapshotDataover a now-weak no-op stub.electron_libdeps it, so the shipped framework links the real snapshot whilenode_mksnapshot(which links onlylibnode+ the weak stub) does not — avoiding the GN dependency cycle thatv8_snapshot_toolchain == default_toolchainwould otherwise force on native builds. Snapshot generation is gated to native builds.Browser-process wiring:
javascript_environment.cc— when the embedded snapshot is present,CreateIsolateHolderfeeds its blob + external references into the isolateCreateParams. The ctor skipsnode::NewContext; the main context is materialized from the snapshot insidenode::CreateEnvironment.electron_browser_main_parts.cc— passes an empty context toInitialize/CreateEnvironmentin the snapshot path, then enters the deserialized context after.node_bindings.cc—CreateEnvironmentdefers context-dependent setup until the snapshot's main context exists;CreateIsolateDatareceives the snapshot's per-isolate data;LoadEnvironmentmerges the build-timeelectron/js2c/*cache with the snapshot's per-builtin cache.Testing
BrowserWindowsmoke test (loads a data URL, runs JS, returns the Electron UA) passes — child processes are unaffected by the browser-only snapshot.spec/api-js2c-code-cache-spec.tsfrom perf: build-time V8 code cache for the electron/js2c framework bundles #51697 passes: the browser process'sbrowser_init/node_initbuild-time code caches are consumed (verified viaNODE_DEBUG_NATIVE=CODE_CACHE— bothis accepted). This required generating the browser cache against the snapshot's isolate (see "Browser js2c code cache" below); the other flavors are byte-identical to perf: build-time V8 code cache for the electron/js2c framework bundles #51697.Browser js2c code cache
A
SnapshotCreator-produced snapshot owns a unique read-only-heap checksum that matches no standalone blob (not thev8_context_snapshot, not thesnapshot_blob.binit extends). V8's code-cache key embeds that checksum, so the build-time js2c cache forbrowser_init/node_initis only valid if it's generated against the shipped snapshot's isolate. A dedicatedelectron_natives_codecache_snapshothost tool links the realnode_snapshotand compiles those bundles viaGetEmbeddedSnapshotData()+InitializeIsolateParams(), so the cache's read-only checksum matches the runtime browser isolate by construction. The four non-browser flavors keep using the v8 context snapshot.Checklist
npm testpassesNotes: Improved app startup time by booting the main process from an embedded Node.js startup snapshot.