detached 1315cb1

docs(perf): add a benchmark and profiling guide

docs/performance.md lists the benchmarks per area, the baseline and benchstat
workflow, CPU and memory profiles, escape analysis, and the oracle and
property checks that a performance change must keep passing.
commit
1315cb1e3619020111801ec2f37ee45367238151
author
brice · · 2026-10-02 16:38 UTC
parent
c6dd596
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +98 -0
M README.md +2 -0
@@ -177,3 +177,5 @@ Go packages retain the module's standard-library-only dependency boundary apart
177177 from `vev-vt/core`.
178178
179179 [Storage benchmarks and design decisions →](docs/storage-optimization.md)
180+
181+[Measuring and profiling performance changes →](docs/performance.md)
@@ -177,3 +177,5 @@ Go packages retain the module's standard-library-only dependency boundary apart
177 from `vev-vt/core`.177 from `vev-vt/core`.
178 178
179 [Storage benchmarks and design decisions →](docs/storage-optimization.md)179 [Storage benchmarks and design decisions →](docs/storage-optimization.md)
180+
181+[Measuring and profiling performance changes →](docs/performance.md)
A docs/performance.md +96 -0
@@ -0,0 +1,96 @@
1+# Performance work
2+
3+How to measure CPU time and allocations before and after a change, and how to
4+check that the change kept behavior.
5+
6+## Workflow
7+
8+1. Pick the benchmark that covers the code you change (table below).
9+2. Record a baseline on the base branch.
10+3. Make the change, then record the same benchmark again.
11+4. Compare with `benchstat`; keep only wins that are stable across runs.
12+5. Run the safety checks below before you commit.
13+
14+```sh
15+# Baseline, from a worktree of the base branch
16+go test ./html -run '^$' -bench '^BenchmarkRenderer$' -benchmem -count=10 > /tmp/old.txt
17+# After the change
18+go test ./html -run '^$' -bench '^BenchmarkRenderer$' -benchmem -count=10 > /tmp/new.txt
19+benchstat /tmp/old.txt /tmp/new.txt
20+```
21+
22+`benchstat` comes from `go install golang.org/x/perf/cmd/benchstat@latest`. Use
23+`-count=10` so it can report variation; a `~` in its output means no
24+significant change.
25+
26+## Benchmarks
27+
28+| Area | Package | Benchmarks |
29+|---|---|---|
30+| VT parsing and screen writes | `.` | `BenchmarkScreenPrintableASCII`, `BenchmarkScreenMixedUTF8`, `BenchmarkScreenCSIHeavy`, `BenchmarkScreenShellRedrawBurst`, `BenchmarkScreenFullscreenScrollRegion`, `BenchmarkScreenKittyAPC` |
31+| Damage capture and resize | `.` | `BenchmarkScreenCaptureDamage`, `BenchmarkScreenResizeReflowViewport` |
32+| History build, views and storage | `.` | `BenchmarkHistoryBuild10Kx120`, `BenchmarkHistoryView10KRows*`, `BenchmarkHistoryRetained10Kx120`, see [storage-optimization.md](storage-optimization.md) |
33+| ANSI renderer | `./ansi` | `BenchmarkRendererFullFrameDraw`, `BenchmarkRendererIncrementalOneCell`, `BenchmarkRendererFragmentedDamage`, `BenchmarkRendererBroadRegularDamage`, `BenchmarkRendererIncrementalNoBytePrepareCommit` |
34+| HTML renderer | `./html` | `BenchmarkRenderer` (`snapshot-*` and `one-row-*` at 80x24, 120x40, 240x80) |
35+
36+`one-row-*` is the common interactive case: one changed line in a full frame.
37+It must stay cheaper than `snapshot-*` at the same size.
38+
39+## Profiles
40+
41+Use a CPU or memory profile to find where the time or bytes go:
42+
43+```sh
44+go test ./html -run '^$' -bench '^BenchmarkRenderer$/one-row-240x80' -benchmem \
45+ -cpuprofile /tmp/cpu.out -memprofile /tmp/mem.out
46+go tool pprof -top /tmp/cpu.out
47+go tool pprof -sample_index=alloc_space -top /tmp/mem.out
48+go tool pprof -http=:0 /tmp/cpu.out # flame graph in the browser
49+```
50+
51+To see why a value escapes to the heap:
52+
53+```sh
54+go build -gcflags=-m ./html 2>&1 | grep -v inlin
55+```
56+
57+Tests can pin an allocation result with `testing.AllocsPerRun`; several
58+packages already do, for example `TestCopyFromDoesNotAllocateOnceSized` in
59+`core`.
60+
61+## Safety checks
62+
63+A faster change must produce the same output:
64+
65+```sh
66+go test ./... # includes TestDifferentialOracle
67+go test -race ./...
68+go vet ./... && staticcheck ./...
69+```
70+
71+`TestDifferentialOracle` digests the output of every layer (VT, history,
72+graphics, ANSI, HTML) for seeded workloads. A performance change must leave
73+`testdata/oracle/golden.txt` unchanged. The property tests in `ansi` and `html`
74+compare reused-buffer renderers with clone-based references through random
75+edits, aborts and resets.
76+
77+When a change reuses buffers, check:
78+
79+- Nothing returned to a caller still points into a reused buffer.
80+- Retained scratch is bounded, so one huge frame does not pin memory.
81+- Reused slices of strings or pointers are cleared, so they do not keep old
82+ values alive.
83+
84+## Browser rendering
85+
86+For the HTML frontend, the browser's style, layout and paint usually cost more
87+than the Go side. The Playwright performance gate in
88+[`internal/htmlharness`](../internal/htmlharness/README.md) bounds sustained
89+snapshot and row work in Chromium:
90+
91+```sh
92+npx playwright test internal/htmlharness/performance.spec.js --project=chromium
93+```
94+
95+Measure user-visible scrolling in the host application, with a real GPU and the
96+user's zoom level: headless Chromium does not reproduce compositor costs.
@@ -0,0 +1,96 @@
1+# Performance work
2+
3+How to measure CPU time and allocations before and after a change, and how to
4+check that the change kept behavior.
5+
6+## Workflow
7+
8+1. Pick the benchmark that covers the code you change (table below).
9+2. Record a baseline on the base branch.
10+3. Make the change, then record the same benchmark again.
11+4. Compare with `benchstat`; keep only wins that are stable across runs.
12+5. Run the safety checks below before you commit.
13+
14+```sh
15+# Baseline, from a worktree of the base branch
16+go test ./html -run '^$' -bench '^BenchmarkRenderer$' -benchmem -count=10 > /tmp/old.txt
17+# After the change
18+go test ./html -run '^$' -bench '^BenchmarkRenderer$' -benchmem -count=10 > /tmp/new.txt
19+benchstat /tmp/old.txt /tmp/new.txt
20+```
21+
22+`benchstat` comes from `go install golang.org/x/perf/cmd/benchstat@latest`. Use
23+`-count=10` so it can report variation; a `~` in its output means no
24+significant change.
25+
26+## Benchmarks
27+
28+| Area | Package | Benchmarks |
29+|---|---|---|
30+| VT parsing and screen writes | `.` | `BenchmarkScreenPrintableASCII`, `BenchmarkScreenMixedUTF8`, `BenchmarkScreenCSIHeavy`, `BenchmarkScreenShellRedrawBurst`, `BenchmarkScreenFullscreenScrollRegion`, `BenchmarkScreenKittyAPC` |
31+| Damage capture and resize | `.` | `BenchmarkScreenCaptureDamage`, `BenchmarkScreenResizeReflowViewport` |
32+| History build, views and storage | `.` | `BenchmarkHistoryBuild10Kx120`, `BenchmarkHistoryView10KRows*`, `BenchmarkHistoryRetained10Kx120`, see [storage-optimization.md](storage-optimization.md) |
33+| ANSI renderer | `./ansi` | `BenchmarkRendererFullFrameDraw`, `BenchmarkRendererIncrementalOneCell`, `BenchmarkRendererFragmentedDamage`, `BenchmarkRendererBroadRegularDamage`, `BenchmarkRendererIncrementalNoBytePrepareCommit` |
34+| HTML renderer | `./html` | `BenchmarkRenderer` (`snapshot-*` and `one-row-*` at 80x24, 120x40, 240x80) |
35+
36+`one-row-*` is the common interactive case: one changed line in a full frame.
37+It must stay cheaper than `snapshot-*` at the same size.
38+
39+## Profiles
40+
41+Use a CPU or memory profile to find where the time or bytes go:
42+
43+```sh
44+go test ./html -run '^$' -bench '^BenchmarkRenderer$/one-row-240x80' -benchmem \
45+ -cpuprofile /tmp/cpu.out -memprofile /tmp/mem.out
46+go tool pprof -top /tmp/cpu.out
47+go tool pprof -sample_index=alloc_space -top /tmp/mem.out
48+go tool pprof -http=:0 /tmp/cpu.out # flame graph in the browser
49+```
50+
51+To see why a value escapes to the heap:
52+
53+```sh
54+go build -gcflags=-m ./html 2>&1 | grep -v inlin
55+```
56+
57+Tests can pin an allocation result with `testing.AllocsPerRun`; several
58+packages already do, for example `TestCopyFromDoesNotAllocateOnceSized` in
59+`core`.
60+
61+## Safety checks
62+
63+A faster change must produce the same output:
64+
65+```sh
66+go test ./... # includes TestDifferentialOracle
67+go test -race ./...
68+go vet ./... && staticcheck ./...
69+```
70+
71+`TestDifferentialOracle` digests the output of every layer (VT, history,
72+graphics, ANSI, HTML) for seeded workloads. A performance change must leave
73+`testdata/oracle/golden.txt` unchanged. The property tests in `ansi` and `html`
74+compare reused-buffer renderers with clone-based references through random
75+edits, aborts and resets.
76+
77+When a change reuses buffers, check:
78+
79+- Nothing returned to a caller still points into a reused buffer.
80+- Retained scratch is bounded, so one huge frame does not pin memory.
81+- Reused slices of strings or pointers are cleared, so they do not keep old
82+ values alive.
83+
84+## Browser rendering
85+
86+For the HTML frontend, the browser's style, layout and paint usually cost more
87+than the Go side. The Playwright performance gate in
88+[`internal/htmlharness`](../internal/htmlharness/README.md) bounds sustained
89+snapshot and row work in Chromium:
90+
91+```sh
92+npx playwright test internal/htmlharness/performance.spec.js --project=chromium
93+```
94+
95+Measure user-visible scrolling in the host application, with a real GPU and the
96+user's zoom level: headless Chromium does not reproduce compositor costs.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close