Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Benchmarks

How fast ashlar is, how that was measured, and how to reproduce it. Machine: Apple M5 Pro (15 cores), 24 GB, macOS. All builds are release builds, run through memgate.

Two kinds of numbers appear below:

  • Quiet measurements were taken with nothing else heavy running. Comparisons use only these.
  • The full-suite snapshot (cargo xtask bench, at the end) ran while other builds shared the machine. Its absolute timings are inflated, often by 2–4×. Allocation counts, sizes and frame counts are not affected by load.

Running them

cargo xtask bench                                   # every `perf_*` test; writes target/bench/bench.{json,md}
cargo xtask bench --filter plain_scroll             # one group
cargo xtask bench --repeat 3 --compare base.json    # fails on a >5 % regression against a saved run
cargo xtask launch --runs 9                         # launch → window → first present, opens real windows
cargo xtask launch --memory                         # idle footprint and peak after a settle

A perf test is a #[test] #[ignore] fn perf_* that prints bench: <metric> <value> <unit> lines (see docs/architecture.md, “Performance gate”).

Against a comparable native Rust UI framework

Measured on 2026-11-10, both frameworks in the same session with the same tools:

  • a launcher that times spawn → window, plus ps and vmmap for idle cost
  • each framework’s own benchmark for the same scrolling workload: a plain list of 1,000 rows of 32 pt in a 760×800 pt viewport at 2×, with 120 warm-up frames and then 480 wheel steps of 12 pt at 120 Hz, reversing halfway

The other framework draws with its own GPU renderer and a retained element tree. ashlar draws with egui.

Metricashlarother frameworkBetter
Scroll 1k plain rows, frame CPU p50 / p95, with list::Row0.10 / 0.12 ms0.298 / 0.32 msashlar, ~2.8×
Same, rows drawn as egui selectable labels0.65–0.76 / 0.70–0.81 ms0.298 / 0.32 msother, ~2.3×
Scroll 10k plain rows, p50, list::Row / labels0.96–1.15 / 8.6–8.9 msnot run—
Stripped hello binary14.0 MB; 9.2 MB with --profile release-small21.3 MBashlar
Incremental rebuild of hello5.3 s14.7–17.7 sashlar, ~3×
Clean release build of hello (loaded machine)133 s133 stie
Launch → window, warm median of 8106.5 ms98 msother, ~8 %
Launch → window, cold first run111 ms1,360 msashlar
Idle CPU over 10 s0 %0 %tie
Idle footprint / peak footprint40 MB / 94 MB (window never covered); 66 MB / 192 MB after the window was covered29 MB / 176 MBother at idle
Dependencies of hello (unique packages)179202ashlar

What each scroll number covers:

  • ashlar’s is the thread CPU of the egui pass plus tessellation.
  • The other framework’s is the thread CPU from wheel dispatch to its draw list.
  • Neither includes GPU submission.

Lists should be virtualized (VirtualList). Then the cost does not depend on the row count at all: 0.035 ms at 1k or 100k rows, measured quiet.

Idle memory, explained: ashlar’s idle footprint is about 40 MB (94 MB peak) for a window that has never been covered, and about 66 MB (about 193 MB peak) once it has been covered, minimized or moved to another Space, even briefly right after launch. At that point macOS maps two swapchain images it shares with the window server into the process (about 19 MB), and Metal allocates a second set of blocks. This happens with any present mode or frame latency, so it is not something ashlar’s own settings cause. cargo xtask launch --memory launches 5 times and reports both groups. The other framework’s 29 MB was measured the same way. Whether its windows avoid the effect, or were simply never covered during the measurement, is not known.

Before and after the 2026-11 improvement work

All quiet measurements. “Before” is commit 393afe0.

MetricBeforeAfterChange
Plain 1k-row list, frame CPU p500.63 ms (labels)0.10 ms (list::Row)6× less
Typing with a search query, 100k notes, keystroke p954.6–5.0 ms0.53 ms9× less
Selecting the last of 100k notes, frame p950.106 ms0.038 msno longer depends on the selected row’s position
Live resize, frame p950.103 ms0.034 ms3× less
Static 52 KB markdown, frame p500.944 ms0.015–0.031 ms30–60× less
Static 52 KB markdown, layout jobs per frame1156
Long 50 KB markdown paragraphquadraticlinear (md_long_paragraph_parse 0.4–0.7 ms)
Galley cache, worst frame after it fills4,160 re-layouts0
One row height change in a 1M-row variable list1,388 µs~1.3 µs
Frames drawn per engine command3.201.2661 % fewer
Frames over 30 s of idle with a focused text field194 (all “input”)21 (caret blinks), then 0
Allocations per idle wake frame95427271 % fewer, gated at ≤ 578
Allocations per keystroke with a query3,716990
Allocations per scroll frame (100k notes)472345
Stripped hello binary12.76 MB (scratch app)9.2 MB with release-small

Component costs (quiet)

MetricValue
VirtualList scroll, 1k / 100k rows0.035 / 0.033 ms
Tree, 1M nodes, scroll frame p950.041 ms
Code view, 100k lines, scrolled frame0.058 ms (first frame about 4.5 ms)
Diff view, 50k lines, scrolled frame0.41 ms (split 0.49 ms)
Chat transcript, 10k messages, frame0.129 ms (1k: 0.092 ms)
Myers diff, 10k lines2.08 ms
Syntax highlighting, 1k lines (Rust / TypeScript / Python)477 / 757 / 280 µs
Headless wgpu screenshot, 2560×1600 including PNG and base6426 ms
Spring sample / rect retarget / OKLab mix7.9 ns / 185 ns / 36 ns
Galley cache hit20 ns
Document edit at 100k elements, chunked vs flat515 ns vs 12,632 ns
Undo or redo14 ns

Full-suite snapshot (loaded machine)

This is cargo xtask bench at imp 30e3264, run while five other builds were compiling. Use it to see which metrics exist, not for absolute timings: run it on a quiet machine before comparing.

MetricValue
allocs_per_idle_wake_frame272 (gate ≤ 578)
bytes_per_frame_scroll_100k203,865 B
notes_first_frame_wall18.3 ms
frame_p95_scroll_100k / tessellate_p95 / frame_cpu_p95_x20.132 / 0.026 / 0.183 ms
frames_per_key_command3.0
plain_scroll_1k_rows_widget_p500.275 ms (0.10 ms quiet)
virtual_scroll_100k_p500.057 ms
md_static_frame_p50 / md_static_layout_jobs0.031 ms / 6
galley_overflow_relayouts0
one_row_height_change_1m1.5 µs
transcript_10k_messages_frame0.111 ms
code_view_100k_lines_frame / diff_50k_lines_frame0.094 / 0.428 ms
hello_stripped_bytes14,009,160 B