detached 5280113

chore(tools): add headless frame-cost bench

commit
5280113a4dbeef7dfe8627f43ffd0e529a8978b7
author
brice · · 2026-10-13 15:29 UTC
committer
brice · · 2026-10-14 04:34 UTC
parent
3b19033
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +89 -0
A tools/bench/README.md +8 -0
@@ -0,0 +1,8 @@
1+# Headless frame-cost bench
2+
3+`bench.sh` measures NeferWL's CPU time and voluntary context switches per presented frame on a 5120x2160 headless output while chromium animates a small part of a page.
4+
5+ tools/bench/bench.sh spin60.html 15 # CSS spinner, 60 fps
6+ tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps
7+
8+Environment: `BENCH_X11=1` runs chromium through XWayland, `BENCH_DEBUG=<categories>` adds `--debug` categories, `BENCH_KEEP=1` leaves the processes running, `BENCH_PPROF=<addr>` sets the pprof address (default `localhost:6061`).
@@ -0,0 +1,8 @@
1+# Headless frame-cost bench
2+
3+`bench.sh` measures NeferWL's CPU time and voluntary context switches per presented frame on a 5120x2160 headless output while chromium animates a small part of a page.
4+
5+ tools/bench/bench.sh spin60.html 15 # CSS spinner, 60 fps
6+ tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps
7+
8+Environment: `BENCH_X11=1` runs chromium through XWayland, `BENCH_DEBUG=<categories>` adds `--debug` categories, `BENCH_KEEP=1` leaves the processes running, `BENCH_PPROF=<addr>` sets the pprof address (default `localhost:6061`).
A tools/bench/bench.sh +79 -0
@@ -0,0 +1,79 @@
1+#!/usr/bin/env bash
2+# Measures NeferWL's CPU cost per presented frame on the headless backend
3+# while chromium animates a small region of a page.
4+#
5+# usage: tools/bench/bench.sh <page.html> <seconds>
6+# env: BENCH_X11=1 run chromium through XWayland instead of Wayland
7+# BENCH_DEBUG=cat extra --debug categories (comma separated)
8+# BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs)
9+# BENCH_PPROF=addr pprof listen address (default localhost:6061)
10+set -euo pipefail
11+
12+page=${1:?usage: bench.sh <page.html> <seconds>}
13+secs=${2:?usage: bench.sh <page.html> <seconds>}
14+here=$(cd "$(dirname "$0")" && pwd)
15+root=$(cd "$here/../.." && pwd)
16+tmp=$(mktemp -d)
17+pids=()
18+
19+cleanup() {
20+ if [ -z "${BENCH_KEEP:-}" ]; then
21+ for p in "${pids[@]}"; do kill -- "-$p" 2>/dev/null || true; done
22+ sleep 1
23+ rm -rf "$tmp"
24+ fi
25+}
26+trap cleanup EXIT
27+
28+(cd "$root" && CGO_ENABLED=0 go build -o "$tmp/nwl" ./cmd/neferwl)
29+mkdir -p "$tmp/rt" "$tmp/cfg" "$tmp/state"
30+chmod 700 "$tmp/rt"
31+
32+debug=render${BENCH_DEBUG:+,$BENCH_DEBUG}
33+xflag=--no-xwayland
34+[ -n "${BENCH_X11:-}" ] && xflag=
35+env XDG_RUNTIME_DIR="$tmp/rt" XDG_CONFIG_HOME="$tmp/cfg" XDG_STATE_HOME="$tmp/state" WAYLAND_DISPLAY= \
36+ setsid "$tmp/nwl" --backend=headless --size=5120x2160 --no-terminal $xflag \
37+ --debug="$debug" --pprof="${BENCH_PPROF:-localhost:6061}" >"$tmp/nwl.out" 2>&1 </dev/null &
38+nwl=$!
39+pids+=("$nwl")
40+log=$tmp/state/neferwl/runs/headless/latest.log
41+for _ in $(seq 50); do [ -S "$tmp/rt/wayland-1" ] && break; sleep 0.1; done
42+
43+if [ -n "${BENCH_X11:-}" ]; then
44+ disp=
45+ for _ in $(seq 50); do
46+ disp=$(grep -o '"DISPLAY":"[^"]*"' "$log" 2>/dev/null | head -1 | cut -d'"' -f4) || true
47+ [ -n "$disp" ] && break
48+ sleep 0.1
49+ done
50+ [ -n "$disp" ] || { echo "no X11 display in log" >&2; exit 1; }
51+ env XDG_RUNTIME_DIR="$tmp/rt" WAYLAND_DISPLAY= DISPLAY="$disp" setsid chromium --ozone-platform=x11 \
52+ --user-data-dir="$tmp/chr" --no-first-run --app="file://$here/$page" >"$tmp/chr.out" 2>&1 </dev/null &
53+else
54+ env XDG_RUNTIME_DIR="$tmp/rt" WAYLAND_DISPLAY=wayland-1 setsid chromium --ozone-platform=wayland \
55+ --user-data-dir="$tmp/chr" --no-first-run --app="file://$here/$page" >"$tmp/chr.out" 2>&1 </dev/null &
56+fi
57+pids+=("$!")
58+sleep 8
59+
60+cs() { cat /proc/"$nwl"/task/*/status | awk '/^voluntary_ctxt/{v+=$2} END{print v}'; }
61+ticks() { awk '{print $14+$15}' /proc/"$nwl"/stat; }
62+frames() { grep -c '"message":"frame"' "$log" || true; }
63+
64+t0=$(ticks); f0=$(frames); c0=$(cs)
65+sleep "$secs"
66+t1=$(ticks); f1=$(frames); c1=$(cs)
67+
68+hz=$(getconf CLK_TCK)
69+python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$hz" "$page" <<'EOF'
70+import sys
71+t, f, c, s, hz, page = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]), float(sys.argv[4]), int(sys.argv[5]), sys.argv[6]
72+cpu = t / hz / s * 100
73+fr = max(f, 1)
74+print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / hz * 1000 / fr:.3f} cs/frame={c / fr:.0f}")
75+EOF
76+
77+if [ -n "${BENCH_KEEP:-}" ]; then
78+ echo "kept: dir=$tmp pids=${pids[*]} log=$log"
79+fi
@@ -0,0 +1,79 @@
1+#!/usr/bin/env bash
2+# Measures NeferWL's CPU cost per presented frame on the headless backend
3+# while chromium animates a small region of a page.
4+#
5+# usage: tools/bench/bench.sh <page.html> <seconds>
6+# env: BENCH_X11=1 run chromium through XWayland instead of Wayland
7+# BENCH_DEBUG=cat extra --debug categories (comma separated)
8+# BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs)
9+# BENCH_PPROF=addr pprof listen address (default localhost:6061)
10+set -euo pipefail
11+
12+page=${1:?usage: bench.sh <page.html> <seconds>}
13+secs=${2:?usage: bench.sh <page.html> <seconds>}
14+here=$(cd "$(dirname "$0")" && pwd)
15+root=$(cd "$here/../.." && pwd)
16+tmp=$(mktemp -d)
17+pids=()
18+
19+cleanup() {
20+ if [ -z "${BENCH_KEEP:-}" ]; then
21+ for p in "${pids[@]}"; do kill -- "-$p" 2>/dev/null || true; done
22+ sleep 1
23+ rm -rf "$tmp"
24+ fi
25+}
26+trap cleanup EXIT
27+
28+(cd "$root" && CGO_ENABLED=0 go build -o "$tmp/nwl" ./cmd/neferwl)
29+mkdir -p "$tmp/rt" "$tmp/cfg" "$tmp/state"
30+chmod 700 "$tmp/rt"
31+
32+debug=render${BENCH_DEBUG:+,$BENCH_DEBUG}
33+xflag=--no-xwayland
34+[ -n "${BENCH_X11:-}" ] && xflag=
35+env XDG_RUNTIME_DIR="$tmp/rt" XDG_CONFIG_HOME="$tmp/cfg" XDG_STATE_HOME="$tmp/state" WAYLAND_DISPLAY= \
36+ setsid "$tmp/nwl" --backend=headless --size=5120x2160 --no-terminal $xflag \
37+ --debug="$debug" --pprof="${BENCH_PPROF:-localhost:6061}" >"$tmp/nwl.out" 2>&1 </dev/null &
38+nwl=$!
39+pids+=("$nwl")
40+log=$tmp/state/neferwl/runs/headless/latest.log
41+for _ in $(seq 50); do [ -S "$tmp/rt/wayland-1" ] && break; sleep 0.1; done
42+
43+if [ -n "${BENCH_X11:-}" ]; then
44+ disp=
45+ for _ in $(seq 50); do
46+ disp=$(grep -o '"DISPLAY":"[^"]*"' "$log" 2>/dev/null | head -1 | cut -d'"' -f4) || true
47+ [ -n "$disp" ] && break
48+ sleep 0.1
49+ done
50+ [ -n "$disp" ] || { echo "no X11 display in log" >&2; exit 1; }
51+ env XDG_RUNTIME_DIR="$tmp/rt" WAYLAND_DISPLAY= DISPLAY="$disp" setsid chromium --ozone-platform=x11 \
52+ --user-data-dir="$tmp/chr" --no-first-run --app="file://$here/$page" >"$tmp/chr.out" 2>&1 </dev/null &
53+else
54+ env XDG_RUNTIME_DIR="$tmp/rt" WAYLAND_DISPLAY=wayland-1 setsid chromium --ozone-platform=wayland \
55+ --user-data-dir="$tmp/chr" --no-first-run --app="file://$here/$page" >"$tmp/chr.out" 2>&1 </dev/null &
56+fi
57+pids+=("$!")
58+sleep 8
59+
60+cs() { cat /proc/"$nwl"/task/*/status | awk '/^voluntary_ctxt/{v+=$2} END{print v}'; }
61+ticks() { awk '{print $14+$15}' /proc/"$nwl"/stat; }
62+frames() { grep -c '"message":"frame"' "$log" || true; }
63+
64+t0=$(ticks); f0=$(frames); c0=$(cs)
65+sleep "$secs"
66+t1=$(ticks); f1=$(frames); c1=$(cs)
67+
68+hz=$(getconf CLK_TCK)
69+python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$hz" "$page" <<'EOF'
70+import sys
71+t, f, c, s, hz, page = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]), float(sys.argv[4]), int(sys.argv[5]), sys.argv[6]
72+cpu = t / hz / s * 100
73+fr = max(f, 1)
74+print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / hz * 1000 / fr:.3f} cs/frame={c / fr:.0f}")
75+EOF
76+
77+if [ -n "${BENCH_KEEP:-}" ]; then
78+ echo "kept: dir=$tmp pids=${pids[*]} log=$log"
79+fi
A tools/bench/emoji.gif +0 -0 binary file: no text diff

binary file: no text diff

A tools/bench/gif.html +1 -0
@@ -0,0 +1,1 @@
1+<html><body style="margin:0;background:#fff;font:14px sans-serif"><p>general — hello <img src="emoji.gif" width=22 height=22> text</p></body></html>
@@ -0,0 +1,1 @@
1+<html><body style="margin:0;background:#fff;font:14px sans-serif"><p>general — hello <img src="emoji.gif" width=22 height=22> text</p></body></html>
A tools/bench/spin60.html +1 -0
@@ -0,0 +1,1 @@
1+<html><body style="margin:0;background:#fff"><p>x</p><div style="width:20px;height:20px;border:3px solid #ccc;border-top-color:#36c;border-radius:50%;animation:s 1s linear infinite"></div><style>@keyframes s{to{transform:rotate(360deg)}}</style></body></html>
@@ -0,0 +1,1 @@
1+<html><body style="margin:0;background:#fff"><p>x</p><div style="width:20px;height:20px;border:3px solid #ccc;border-top-color:#36c;border-radius:50%;animation:s 1s linear infinite"></div><style>@keyframes s{to{transform:rotate(360deg)}}</style></body></html>

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 25

Tags 10

↑↓ moveenter openesc close