#52923: perf: cut first-window, ipc, contextBridge, Buffer and asar overhead
Description of Change
Eight independent commits, batched to be easier on CI. Together on a Linux test app (main process loading ~1000 modules from app.asar, one window with a sandboxed preload): launch to first contentful paint 633 -> 452 ms, MB-scale IPC a third cheaper in both directions, bridged calls 25-57% cheaper, Buffer.allocUnsafe 6-80x cheaper, large files from the asar to the renderer as fast as plain file://. Each commit stands alone; its message carries the mechanism and the full numbers. Linux x64 release build, medians of interleaved fresh-process runs, all p < 0.01.
| commit | what gets faster | before -> after |
|---|---|---|
| preallocate the file descriptor table before threads start (linux) | first new BrowserWindow() on Linux; launch to first contentful paint |
100 -> 18 ms; 610 -> 525 ms |
stop rebuilding the whole path per component in asar::GetAsarArchivePath |
every fs lookup on a path inside an .asar (asar.splitPath()); launch to ready for the test app |
5.3 -> 1.3 us; 393 -> 347 ms |
| keep context bridge proxy function state in the function's data array + trim per-value work when the bridge copies objects and arrays | calls through contextBridge-exposed APIs: empty call / callback round trip / 400-item list |
0.30 -> 0.13 us / 2.7 -> 1.9 us / 757 -> 684 us |
| give V8's in-sandbox allocator a PartitionAlloc backend outside renderers | Buffer.allocUnsafe 1 MB; fs.promises.readFile 1 MB in the main process |
297 -> 36 us; 1.2 -> 0.9 ms |
| serialize ipc values straight into the buffer they are sent in | 1 MB ipcRenderer.invoke round trip; main -> renderer 1 MB; 4 MB round trip |
4.2 -> 2.8 ms; 2.7 -> 1.6 ms; 14.2 -> 9.2 ms |
| size protocol and asar response pipes to the body | 16 MB file from an .asar to the renderer; 16 MB protocol.handle body with a Content-Length |
35 -> 22 ms; 49 -> 40 ms |
| remember module stat results for paths inside an archive | repeated internalModuleStat misses during require() from an archive (1040 modules) |
250 -> 245 ms |
Per commit, briefly:
-
fd table (Linux only). The browser crosses 64 open descriptors while setting up the first renderer, and Linux's
expand_fdtable()waits a full RCU grace period before freeing the old table once the process has threads; that wait was 55-116 ms of uninterruptible sleep inside window creation on the test host (10-30 ms is typical on a workstation). content's zygote already grows the table for the children it forks;main()now does the same for the browser and exec'd children with oneF_DUPFD_CLOEXECwhile still single-threaded (1024 entries, 8 KB). -
GetAsarArchivePath. Walks component boundaries as offsets instead ofDirName()/MatchesExtension()per component plusGetComponents()twice; byte-identical outputs, three new asar-spec cases for the corner cases. -
contextBridge. Proxy function state moves from three private-property lookups per call to a three-slot array in the function's data; per copied value, the inner trace event goes, the Blink wrapper probes only run for API wrappers, one scope pair per property, bulk
v8::Array::New. What is copied or proxied and all error paths are unchanged. -
in-sandbox allocator. Node allocates unpooled Buffers through V8's default in-sandbox allocator, which V8 documents as a slow placeholder (page-granular, global mutex, zero-fills everything); Blink replaces it for renderers, the browser/utility/run-as-node processes never did.
JavascriptEnvironmentnow installs one backed by gin's array-buffer partition viav8::V8::SetInSandboxAllocator, with allocations PartitionAlloc refuses (~2 GiB and up) served from the sandbox address space so hugeBuffer.allocUnsafecalls keep working. Give-back: page zeroing moves to first touch, soBuffer.concatof MB inputs measures a few percent slower, which is upstream Node's behavior. -
ipc payloads. Electron's own IPC mojom switches from
blink.mojom.CloneableMessagetoelectron.mojom.SerializedValue(BigBuffer+ length): the V8 serializer writes large values straight into the shared memory region that is sent, and a renderer parses a received region in place; the browser still parses a private copy because the sender keeps write access to shared memory. New api-ipc spec round-trips payloads across the 64 KB threshold up to 3 MB. Small messages,MessagePorttraffic and startup unchanged. -
response pipes. Electron's URL loaders used mojo's 64 KB default pipe (the asar loader a hard-coded 65536); where the body length is known before the pipe is created it is sized to the body with upstream's caps (2 MB for archive entries like
file://, 512 KB for string/buffer andContent-Lengthresponses); unknown-length streams and small responses keep exactly the pipe they had. -
module stat cache. Node's own
statCacheonly lives for one depth-0 execution, so repeatedinternalModuleStatmisses on archive paths across separaterequirechains re-probe the archive; a bounded map keyed by the exact path remembers answers derived from an already-open (immutable) header. Small on a single startup graph, useful for lazy-requirepatterns.
Testing: full main-process suite identical to the baseline before/after with all seven applied; ASAN pass over the C++ commits clean; the asar and ipc commits add specs.
Checklist
- I have built and tested this change
- I have filled out the PR description
- I have reviewed and verified the changes
-
npm testpasses - tests are changed or added
- PR release notes describe the change in a way relevant to app developers, and are capitalized, punctuated, and past tense.
Release Notes
Notes:
- Fixed a stall of tens of milliseconds while creating the first
BrowserWindowon Linux. - Improved the performance of file lookups inside ASAR archives and of large files served from them to the renderer.
- Improved the performance of calls through
contextBridge-exposed APIs. - Improved the performance of
Buffer.allocUnsafe()and of large file reads in the main and utility processes. - Reduced the main-thread cost of sending large values through
ipcRenderer,ipcMainhandlers andwebContents.send.
Backports
Semver Impact
Semantic Versioning helps users understand the impact of updates:
- Major (X.y.z): Breaking changes that may require code modifications
- Minor (x.Y.z): New features that maintain backward compatibility
- Patch (x.y.Z): Bug fixes that don't change the API
- None: Changes that don't affect using facing parts of Electron