zgx

Profiling

ZGX_PROFILE splits a frame by phase; the perf suite compares two revisions in retired instructions.

Measure in retired instructions, not time: a fixed workload’s count repeats to well under 0.5%, where its time moves by several percent with the machine’s state. Time and GPU numbers come from the windowed comparison (scripts/run_macos_comparison.py) on a quiet desktop.

A change’s cost, in two minutes

python3 scripts/perf/suite.py                  # every headless scene, this checkout
python3 scripts/perf/suite.py ab origin/main . # main against this checkout's files
python3 scripts/perf/suite.py ab abc123 def456 # two commits
python3 scripts/perf/suite.py --only heavy ab origin/main .

The suite runs each headless scene (heavy dashboard in three modes, lab stream and scroll, animate, script mount, anime, layout, list state, GPU layout, frame, text, style, widgets; --all adds the 10-second state and core benches) a few times and reports the least disturbed run’s instructions with the spread above it. ab builds each revision in a worktree of its own under /tmp/zgx-perf (kept: the next comparison builds incrementally), interleaves the runs, and marks differences larger than either side’s spread. suite.py dirs A B compares two directories of examples built otherwise from the same source (other RUSTFLAGS, a profile, an allocator). suite.py clean removes the worktrees.

Where a frame’s work goes: ZGX_PROFILE=1

Any zgx app or bench run with ZGX_PROFILE=1 prints, every 120 frames, what each phase cost per frame:

profile 120 frames: | update 3555.4k instr 191.5 us | layout 3182.2k instr 145.0 us | damage 2285.1k instr 134.2 us | paint 3090.2k instr 324.5 us | encode 737.8k instr 35.0 us | submit 17.5k instr 4.6 us

Each phase’s numbers leave out the phases it encloses (text shaping during damage resolution counts as damage). Instructions are the calling thread’s. Unset, a phase boundary costs one relaxed load, and the suite shows no scene moving (docs/benchmarking.md). Building with --features zgx/profile-nested also times styles and text within their phases (a check in every style and text run, so never in normal builds). On macOS the phases are os_signpost intervals (subsystem dev.zgx): record with Instruments’ os_signpost instrument to see them on a timeline beside a Time Profiler. At startup it also prints a line per milestone (window created, renderer made, first frame presented, …) with the process’s instructions and time so far (zgx::profile::mark).

Where a phase’s work goes: samples

scripts/perf/record.sh /tmp/heavy.trace 5 -- target/release/examples/heavy_headless
scripts/perf/record.sh /tmp/app.trace 10 --attach $(pgrep -n my_app)
python3 scripts/perf/samples.py /tmp/heavy.xml --children=render_once
python3 scripts/perf/samples.py /tmp/heavy.xml --under=Host::draw --incl
python3 scripts/perf/samples.py /tmp/app.xml --threads
python3 scripts/perf/lines.py /tmp/heavy.xml target/release/examples/heavy_headless render_flat
python3 scripts/perf/frames.py target/release/examples/heavy_headless zgx::scene

record.sh wraps xctrace’s Time Profiler and exports the samples. samples.py aggregates them by function: self, inclusive, under a function, its callees, its callers, or per thread. lines.py resolves the samples inside one function to source lines and inlined helpers (build with CARGO_PROFILE_RELEASE_DEBUG=line-tables-only). frames.py lists stack frame sizes, which finds functions paying for large values on the stack.

Profile-guided builds

python3 scripts/perf/pgo.py -p PACKAGE --bin NAME [TRAINING COMMAND ...] builds a dist binary trained on zgx’s headless scenes (and the commands, {bin} being the instrumented binary) into target/dist-pgo/. docs/getting-started.md has the recipe for an app outside the repository. The published comparison builds neither zgx nor GPUI this way.

The real window path, quickly

python3 scripts/perf/preflight.py           # is the machine quiet? what runs?
python3 scripts/perf/preflight.py --state   # displays, wallpaper, power mode, build, uptime
python3 scripts/run_macos_comparison.py --scene heavy --quick   # ~10 min, zgx vs GPUI Fast

The comparison waits for the preflight, hides Zeron, and records both in its audit; see docs/benchmarking.md.

The window server

scripts/windowserver_pair.py and scripts/windowserver_alternate.py measure the window server’s CPU for two apps, or two configurations of one, in paired seconds; see docs/performance-feature-audit.md.

Edit on GitHub