Back to Electron

Electron PGO profile generation

script/pgo/README.md

42.3.26.5 KB
Original Source

Electron PGO profile generation

This directory contains the infrastructure for generating Electron-specific PGO (profile-guided optimization) profiles. Two kinds of profile are generated:

  1. C++ binary profiles (.profdata) - guide clang's optimization of all compiled C++ (Chromium, V8's C++, Node.js, the Electron shell).
  2. V8 builtins profiles (.profile) - guide mksnapshot's basic-block ordering of the V8 builtins baked into the snapshot (property access ICs, Array.prototype.*, promise machinery, etc.).

Why Electron needs its own profiles

Electron release builds use chrome_pgo_phase = 2, which applies a PGO profile during compilation. Today that profile is the one Chromium publishes for Chrome (downloaded by tools/update_pgo_profiles.py). That profile is collected on Chrome's binary - every function that differs in Electron (patched Chromium code, all of Node.js, the Electron shell, V8 built with Node's flags) silently loses profile guidance.

Measured on Speedometer 3.1 (Linux x64), replacing Chrome's profile with an Electron-collected profile is worth roughly +9%.

The same problem applies to V8 builtins: Chrome's published builtins profiles reject Electron's promise/async builtins (RunMicrotasks, AsyncFunctionAwait, FulfillPromise, PromiseConstructor, ...) because Electron's Node.js integration changes their code generation (v8_promise_internal_field_count = 1, v8_enable_javascript_promise_hooks = true). An Electron-generated builtins profile covers every builtin with zero rejections.

How profiles are generated

The process mirrors Chromium's own PGO recipe (tools/pgo/generate_profile.py), extended with Electron-specific workloads:

  1. Build an instrumented Electron: build/args/pgo-instrument.gn (chrome_pgo_phase = 1).
  2. Run script/pgo/collect-profile.js, which:
    • serves Speedometer 3, JetStream 2, and MotionMark from a local HTTPS/HTTP-2 server (pinned revisions, no external network during collection; an ephemeral CA is installed into the host trust store so the real certificate verification path is trained);
    • launches the instrumented build with LLVM_PROFILE_FILE set and drives every workload via script/pgo/benchmark-app:
      • browser benchmarks - Speedometer 3 (x3), JetStream 2, MotionMark (the same set Chromium's PGO bots run);
      • main-process workload - Node.js Buffer/crypto/fs/JSON loops. Browser benchmarks never execute Node-only native code, which would otherwise be cold-marked by the profile (measured at -63% on Buffer operations without this phase);
      • IPC + contextBridge workload - bridge marshaling and ipcRenderer.invoke round trips at multiple payload sizes;
      • network workload - renderer fetch/WebSocket loops plus Node-side HTTPS, training Chromium's TLS/H2/network-service code and Node's separate TLS/HTTP stack;
    • relies on a clean app.quit() so every Electron process writes its counters (killed processes write nothing);
    • merges the resulting .profraw files with llvm-profdata into a single .profdata.
  3. Merge and upload. In CI, collection jobs produce raw .profraw artifacts (collection hosts can't always run the x64 LLVM tooling - e.g. Linux arm64 runners); the merge-profiles job then merges each platform/arch's set into a .profdata using the in-tree llvm-profdata tooling (the merge job restores the Linux src cache and runs Chromium's standard tools/clang/scripts/update.py --package=coverage_tools), and the upload-profiles job authenticates to Azure with OIDC through the pgo-upload GitHub environment and uploads to the pgo container of the build-tools storage account. Uploaded profiles are served at https://dev-cdn.electronjs.org/pgo/<profile-name>.

How V8 builtins profiles are generated

The process mirrors upstream V8's recipe (v8/tools/builtins-pgo/generate.py):

  1. Build d8 with Electron's exact V8 configuration plus builtins profiling: build/args/pgo-builtins-instrument.gn (v8_enable_builtins_profiling = true). Configuration matching is mandatory - builtin block-graph hashes depend on V8 codegen flags, and a profile generated with the wrong configuration is rejected by mksnapshot.
  2. Run JetStream 2 under d8 with --turbo-profiling-output=<log> (the same workload upstream V8 uses for its profiles).
  3. Convert the log to block hints with v8/tools/builtins-pgo/get_hints.py.

Builtin block graphs are arch-independent, so - mirroring upstream V8, where arm64 release builds consume the x64 profile - one x64-generated profile covers Electron's x64 and arm64 builds. 32-bit targets (win-x86, linux-arm) keep using upstream's x86 profile until 32-bit generation is added.

Running locally

sh
# 1. Build the instrumented Electron (example with build-tools)
e init pgo-instrument --root=$PWD --import pgo-instrument
e build

# 2. Collect (Linux needs a display, e.g. under xvfb-run)
xvfb-run -a -s "-screen 0 1280x1024x24" \
  node electron/script/pgo/collect-profile.js \
    --electron out/Release/electron \
    --output out/Release/electron.profdata

The collection takes roughly 30-60 minutes (instrumented builds run at about half speed).

For the V8 builtins profile:

sh
# 1. Build instrumented d8
e init pgo-builtins --root=$PWD --import pgo-builtins-instrument
e build --target v8:d8

# 2. Run JetStream 2 under d8 and convert to block hints
git clone --depth 1 https://github.com/WebKit/JetStream.git /tmp/jetstream
cd /tmp/jetstream
<out>/d8 --no-sandbox-prohibit-insecure-mode \
  --turbo-profiling-output=/tmp/v8.builtins.log cli.js
python3 <src>/v8/tools/builtins-pgo/get_hints.py \
  /tmp/v8.builtins.log electron-v8-x64.profile

Using generated profiles

Point release builds at the profiles with:

gn
# C++ binary profile
pgo_data_path = "//path/to/electron-<platform>-<arch>-....profdata"

# V8 builtins profile (must be an in-tree path for remote-exec builds)
v8_builtins_profiling_log_file = "//v8/tools/builtins-pgo/profiles/electron-v8-x64.profile"

Platform notes

Profiles are generated for every published Electron platform/arch - each combo's instrumented build runs its own benchmarks:

ProfileCollection host
linux-x64x64 ARC runner (build container)
linux-armarm64 ARC runner (arm32v7 test container)
linux-arm64ubuntu-22.04-arm (arm64v8 test container)
win-x64windows-latest
win-x86windows-latest (WOW64)
win-arm64windows-11-arm
macos-x64macos-15-large
macos-arm64macos-15