CI build, for e2e-test and nightly now build with: -Dorderfile/lightpanda.ld This file informs the build on how to organize the code in the binary, grouping hot code together so that we have to load less of the binary into memory. lightpanda.ld will drift: we'll refactor our code, add new features, update dependencies, update Zig, ... So it has to be re-generated. But we can do that automatically in the CI (say, before the nightly build). That's for a follow up PR. This does not currently cover V8. V8 is being build with `-no-unique-section-names`, so we don't get names that we can correctly organize. The real win comes from doing this in V8, since a lot of V8 is cold. This PR can land as-is, a zig-v8-fork PR will remove that flag, and then we can have a follow up PR with an lightpanda.ld that includes the v8 symbols. This is opt-in (via the -Dorderfile flag) because it adds ~20 seconds of linking time.
Hot-code orderfile
lightpanda.ld is an LLD linker script that gathers the functions and
read-only data touched by a typical session into two contiguous output
sections, .text.hot and .rodata.hot, placed in front of the cold .text
and .rodata. It only affects layout; no code changes.
Why
The kernel maps file-backed pages in 64KB fault-around windows
(/sys/kernel/debug/fault_around_bytes). With the default link order the hot
code is scattered across the whole 40MB of text, so every window that holds a
single executed byte becomes resident: ~17MB of .text and ~4MB of .rodata
for a hot set that is really 7.5MB + 2MB. Packing the hot set contiguously
takes the CDP bench (demo/puppeteer/cdp.js, 100 runs) from VmHWM ~28.2MB to
~25.3MB with the stock V8 archive and ~21.2MB once V8 is addressable too (see
below), with no change in run duration.
How it is applied
- Opt-in via
-Dorderfile=orderfile/lightpanda.ld; CI passes it for the Linux release artifact and the e2e bench build. Local release builds don't, so they keep the sub-second link. Linux/LLD only (it is an ELF linker script). - The exe and every C library it links are built with
-ffunction-sections -fdata-sectionsso each function/datum has its own input section for the script to address. - Patterns that match nothing are ignored by LLD, so a stale script degrades
gracefully: renamed or removed functions simply fall back into cold
.text. Zig's__anon_NNNnames (~7% of the patterns) renumber on unrelated changes, so the profile decays a little with every commit; regenerate it when CI's VmHWM creeps. - Link-time cost: LLD name-checks every input section against every pattern
of every input-section description, so the script scopes patterns to their
object file (
*api.o(...)). The shipped script covers Zig/Rust/C only (~4k patterns, ~2s of link). Once V8's functions are addressable (below) the script grows to ~26k patterns and the release link to ~26s; one pattern per line instead of per file would make that 85s. Debug builds pay nothing. - The prebuilt V8 archive is compiled with
-fno-unique-section-names(Chromium hardcodes it), so all of V8's function sections are named.textand the script cannot address them.tools/uniqar.pyrewrites the archive so each section is named.text.<symbol>(.rodata.<symbol>); link against that copy via-Dprebuilt_v8_path=.... Without it the script still orders the Zig, Rust and C code, with a smaller gain.
Regenerating the profile
The profile is a list of resident pages captured at 4KB granularity, so fault-around has to be disabled while profiling (root):
# 1. Build with the current script (or none) and copy the binary to tmpfs.
# Bench from tmpfs: on ext4 with a recent kernel, large page-cache folios
# are mapped whole and hide the 64KB-window behaviour that CI sees.
zig build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64 \
-Dorderfile=orderfile/lightpanda.ld
cp zig-out/bin/lightpanda /tmp/lightpanda
# 2. Run the workload with fault-around off and dump which pages are resident.
sudo sh -c 'echo 4096 > /sys/kernel/debug/fault_around_bytes'
/tmp/lightpanda serve --insecure-disable-tls-host-verification & LP=$!
(cd ../demo && RUNS=100 node puppeteer/cdp.js)
sleep 5
python3 orderfile/tools/pagemap.py $LP resident.json
kill $LP
sudo sh -c 'echo 65536 > /sys/kernel/debug/fault_around_bytes'
# 3. Resident pages -> hot symbol lists (address order), then -> linker script.
# gen_order.py needs every object/archive of the link to map symbols to
# section names: `zig build ... --verbose-link` prints the ld.lld line.
python3 orderfile/tools/hotlist.py /tmp/lightpanda resident.json hot
python3 orderfile/tools/gen_order.py hot.text hot.rodata orderfile/lightpanda.ld \
<uniq-v8.a> .zig-cache/o/*/lightpanda_zcu.o .zig-cache/o/*/*.a ...
Profiles from several runs can be concatenated before gen_order.py
(duplicates are dropped, first occurrence wins); the shipped script is the
union of a profile of the unordered binary and one of the ordered binary.
To check the result without root, compare VmHWM from /proc/<pid>/status
after the bench, and look at what is still resident outside the hot sections
with pagemap.py + nm.