detached 91d21b6

chore(bench): add idle page, per-thread wakes and A/B root

commit
91d21b68adcaa7e682ef3dedcef93a704b13788e
author
brice · · 2026-10-13 19:16 UTC
committer
brice · · 2026-10-13 20:12 UTC
parent
5affde4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +26 -6
M go.mod +3 -0
@@ -24,3 +24,6 @@ require (
2424 go.yaml.in/yaml/v3 v3.0.5 // indirect
2525 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
2626 )
27+
28+// Prototype requires the unreleased netpoller display loop.
29+replace github.com/bnema/purego-libwayland => ../../../purego-libwayland/.worktrees/proto-netpoll
@@ -24,3 +24,6 @@ require (
24 go.yaml.in/yaml/v3 v3.0.5 // indirect24 go.yaml.in/yaml/v3 v3.0.5 // indirect
25 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect25 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
26 )26 )
27+
28+// Prototype requires the unreleased netpoller display loop.
29+replace github.com/bnema/purego-libwayland => ../../../purego-libwayland/.worktrees/proto-netpoll
M tools/bench/README.md +2 -0
@@ -6,3 +6,5 @@
66 tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps
77
88 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`).
9+
10+`idle.html` is a static page: run it with `BENCH_IDLE=1 tools/bench/bench.sh idle.html 15` (no frame is required). The output also reports `cs/s` and, for the thread with the most voluntary switches (usually the Go runtime's sysmon), its scheduler wakes per second (`top_wakes/s`, schedstat field 3). `BENCH_ROOT=<worktree>` builds neferwl from another worktree for A/B runs.
@@ -6,3 +6,5 @@
6 tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps6 tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps
7 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`).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`).
9+
10+`idle.html` is a static page: run it with `BENCH_IDLE=1 tools/bench/bench.sh idle.html 15` (no frame is required). The output also reports `cs/s` and, for the thread with the most voluntary switches (usually the Go runtime's sysmon), its scheduler wakes per second (`top_wakes/s`, schedstat field 3). `BENCH_ROOT=<worktree>` builds neferwl from another worktree for A/B runs.
M tools/bench/bench.sh +18 -6
@@ -7,12 +7,14 @@
77 # BENCH_DEBUG=cat extra --debug categories (comma separated)
88 # BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs)
99 # BENCH_PPROF=addr pprof listen address (default localhost:6061)
10+# BENCH_ROOT=dir build neferwl from another worktree (A/B runs)
11+# BENCH_IDLE=1 static page: no frames expected, per-second numbers only
1012 set -euo pipefail
1113
1214 page=${1:?usage: bench.sh <page.html> <seconds>}
1315 secs=${2:?usage: bench.sh <page.html> <seconds>}
1416 here=$(cd "$(dirname "$0")" && pwd)
15-root=$(cd "$here/../.." && pwd)
17+root=${BENCH_ROOT:-$(cd "$here/../.." && pwd)}
1618 tmp=$(mktemp -d)
1719 pids=()
1820
@@ -67,22 +69,32 @@ cs() { cat /proc/"$nwl"/task/*/status | awk '/^voluntary_ctxt/{v+=$2} END{print
6769 cpu_ns() { cat /proc/"$nwl"/task/*/schedstat | awk '{v+=$1} END{printf "%d\n", v}'; }
6870 frames() { grep -c '"message":"frame"' "$log" || true; }
6971
70-t0=$(cpu_ns); f0=$(frames); c0=$(cs)
72+# Per-thread "tid sched_in_count voluntary_switches" (schedstat field 3 = times scheduled in).
73+threads() { for d in /proc/"$nwl"/task/*; do
74+ echo "${d##*/} $(awk '{print $3}' "$d/schedstat" 2>/dev/null) $(awk '/^voluntary_ctxt/{print $2}' "$d/status" 2>/dev/null)"
75+done; }
76+
77+th0=$(threads); t0=$(cpu_ns); f0=$(frames); c0=$(cs)
7178 sleep "$secs"
72-t1=$(cpu_ns); f1=$(frames); c1=$(cs)
79+th1=$(threads); t1=$(cpu_ns); f1=$(frames); c1=$(cs)
7380
74-if [ "$((f1 - f0))" -le 0 ]; then
81+if [ -z "${BENCH_IDLE:-}" ] && [ "$((f1 - f0))" -le 0 ]; then
7582 echo "no frame presented in ${secs}s: the page did not animate or neferwl stalled" >&2
7683 tail -n 20 "$tmp/nwl.out" >&2
7784 exit 1
7885 fi
7986
80-python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$page" <<'EOF'
87+python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$page" "$th0" "$th1" <<'EOF'
8188 import sys
8289 t, f, c, s, page = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]), float(sys.argv[4]), sys.argv[5]
90+def parse(x):
91+ return {l.split()[0]: tuple(int(v) for v in l.split()[1:]) for l in x.splitlines() if len(l.split()) == 3}
92+a, b = parse(sys.argv[6]), parse(sys.argv[7])
93+d = {k: (b[k][0] - a.get(k, (0, 0))[0], b[k][1] - a.get(k, (0, 0))[1]) for k in b}
94+top = max(d, key=lambda k: d[k][1])
8395 cpu = t / 1e9 / s * 100
8496 fr = max(f, 1)
85-print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / 1e6 / fr:.3f} cs/frame={c / fr:.0f}")
97+print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / 1e6 / fr:.3f} cs/frame={c / fr:.0f} cs/s={c / s:.0f} top_tid={top} top_wakes/s={d[top][0] / s:.0f} top_vol/s={d[top][1] / s:.0f}")
8698 EOF
8799
88100 if [ -n "${BENCH_KEEP:-}" ]; then
@@ -7,12 +7,14 @@
7 # BENCH_DEBUG=cat extra --debug categories (comma separated)7 # BENCH_DEBUG=cat extra --debug categories (comma separated)
8 # BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs)8 # BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs)
9 # BENCH_PPROF=addr pprof listen address (default localhost:6061)9 # BENCH_PPROF=addr pprof listen address (default localhost:6061)
10+# BENCH_ROOT=dir build neferwl from another worktree (A/B runs)
11+# BENCH_IDLE=1 static page: no frames expected, per-second numbers only
10 set -euo pipefail12 set -euo pipefail
11 13
12 page=${1:?usage: bench.sh <page.html> <seconds>}14 page=${1:?usage: bench.sh <page.html> <seconds>}
13 secs=${2:?usage: bench.sh <page.html> <seconds>}15 secs=${2:?usage: bench.sh <page.html> <seconds>}
14 here=$(cd "$(dirname "$0")" && pwd)16 here=$(cd "$(dirname "$0")" && pwd)
15-root=$(cd "$here/../.." && pwd)17+root=${BENCH_ROOT:-$(cd "$here/../.." && pwd)}
16 tmp=$(mktemp -d)18 tmp=$(mktemp -d)
17 pids=()19 pids=()
18 20
@@ -67,22 +69,32 @@ cs() { cat /proc/"$nwl"/task/*/status | awk '/^voluntary_ctxt/{v+=$2} END{print
67 cpu_ns() { cat /proc/"$nwl"/task/*/schedstat | awk '{v+=$1} END{printf "%d\n", v}'; }69 cpu_ns() { cat /proc/"$nwl"/task/*/schedstat | awk '{v+=$1} END{printf "%d\n", v}'; }
68 frames() { grep -c '"message":"frame"' "$log" || true; }70 frames() { grep -c '"message":"frame"' "$log" || true; }
69 71
70-t0=$(cpu_ns); f0=$(frames); c0=$(cs)72+# Per-thread "tid sched_in_count voluntary_switches" (schedstat field 3 = times scheduled in).
73+threads() { for d in /proc/"$nwl"/task/*; do
74+ echo "${d##*/} $(awk '{print $3}' "$d/schedstat" 2>/dev/null) $(awk '/^voluntary_ctxt/{print $2}' "$d/status" 2>/dev/null)"
75+done; }
76+
77+th0=$(threads); t0=$(cpu_ns); f0=$(frames); c0=$(cs)
71 sleep "$secs"78 sleep "$secs"
72-t1=$(cpu_ns); f1=$(frames); c1=$(cs)79+th1=$(threads); t1=$(cpu_ns); f1=$(frames); c1=$(cs)
73 80
74-if [ "$((f1 - f0))" -le 0 ]; then81+if [ -z "${BENCH_IDLE:-}" ] && [ "$((f1 - f0))" -le 0 ]; then
75 echo "no frame presented in ${secs}s: the page did not animate or neferwl stalled" >&282 echo "no frame presented in ${secs}s: the page did not animate or neferwl stalled" >&2
76 tail -n 20 "$tmp/nwl.out" >&283 tail -n 20 "$tmp/nwl.out" >&2
77 exit 184 exit 1
78 fi85 fi
79 86
80-python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$page" <<'EOF'87+python3 - "$((t1 - t0))" "$((f1 - f0))" "$((c1 - c0))" "$secs" "$page" "$th0" "$th1" <<'EOF'
81 import sys88 import sys
82 t, f, c, s, page = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]), float(sys.argv[4]), sys.argv[5]89 t, f, c, s, page = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]), float(sys.argv[4]), sys.argv[5]
90+def parse(x):
91+ return {l.split()[0]: tuple(int(v) for v in l.split()[1:]) for l in x.splitlines() if len(l.split()) == 3}
92+a, b = parse(sys.argv[6]), parse(sys.argv[7])
93+d = {k: (b[k][0] - a.get(k, (0, 0))[0], b[k][1] - a.get(k, (0, 0))[1]) for k in b}
94+top = max(d, key=lambda k: d[k][1])
83 cpu = t / 1e9 / s * 10095 cpu = t / 1e9 / s * 100
84 fr = max(f, 1)96 fr = max(f, 1)
85-print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / 1e6 / fr:.3f} cs/frame={c / fr:.0f}")97+print(f"{page} cpu%={cpu:.2f} fps={f / s:.1f} ms/frame={t / 1e6 / fr:.3f} cs/frame={c / fr:.0f} cs/s={c / s:.0f} top_tid={top} top_wakes/s={d[top][0] / s:.0f} top_vol/s={d[top][1] / s:.0f}")
86 EOF98 EOF
87 99
88 if [ -n "${BENCH_KEEP:-}" ]; then100 if [ -n "${BENCH_KEEP:-}" ]; then
A tools/bench/idle.html +3 -0
@@ -0,0 +1,3 @@
1+<!doctype html>
2+<html><head><meta charset="utf-8"><title>idle</title></head>
3+<body style="margin:0;background:#202030;color:#ddd;font:24px sans-serif"><p>NeferWL idle benchmark: static page, nothing animates.</p></body></html>
@@ -0,0 +1,3 @@
1+<!doctype html>
2+<html><head><meta charset="utf-8"><title>idle</title></head>
3+<body style="margin:0;background:#202030;color:#ddd;font:24px sans-serif"><p>NeferWL idle benchmark: static page, nothing animates.</p></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 24

Tags 10

↑↓ moveenter openesc close