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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +26 -6
M go.mod +3 -0
| @@ -24,3 +24,6 @@ require ( | |||
| 24 | 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | |
| 25 | 25 | 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 | |
| @@ -24,3 +24,6 @@ require ( | |||||
| 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | ||
| 25 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | 25 | 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 @@ | |||
| 6 | 6 | tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps | |
| 7 | 7 | ||
| 8 | 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. | |
| @@ -6,3 +6,5 @@ | |||||
| 6 | tools/bench/bench.sh gif.html 15 # 32x32 GIF, 20 fps | 6 | 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 @@ | |||
| 7 | 7 | # BENCH_DEBUG=cat extra --debug categories (comma separated) | |
| 8 | 8 | # BENCH_KEEP=1 leave neferwl and chromium running (prints PIDs) | |
| 9 | 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 | 12 | set -euo pipefail | |
| 11 | 13 | ||
| 12 | 14 | page=${1:?usage: bench.sh <page.html> <seconds>} | |
| 13 | 15 | secs=${2:?usage: bench.sh <page.html> <seconds>} | |
| 14 | 16 | here=$(cd "$(dirname "$0")" && pwd) | |
| 15 | - | root=$(cd "$here/../.." && pwd) | |
| 17 | + | root=${BENCH_ROOT:-$(cd "$here/../.." && pwd)} | |
| 16 | 18 | tmp=$(mktemp -d) | |
| 17 | 19 | pids=() | |
| 18 | 20 | ||
| @@ -67,22 +69,32 @@ cs() { cat /proc/"$nwl"/task/*/status | awk '/^voluntary_ctxt/{v+=$2} END{print | |||
| 67 | 69 | cpu_ns() { cat /proc/"$nwl"/task/*/schedstat | awk '{v+=$1} END{printf "%d\n", v}'; } | |
| 68 | 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 | 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 ]; then | |
| 81 | + | if [ -z "${BENCH_IDLE:-}" ] && [ "$((f1 - f0))" -le 0 ]; then | |
| 75 | 82 | echo "no frame presented in ${secs}s: the page did not animate or neferwl stalled" >&2 | |
| 76 | 83 | tail -n 20 "$tmp/nwl.out" >&2 | |
| 77 | 84 | exit 1 | |
| 78 | 85 | 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 | 88 | import sys | |
| 82 | 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 | 95 | cpu = t / 1e9 / s * 100 | |
| 84 | 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 | 98 | EOF | |
| 87 | 99 | ||
| 88 | 100 | 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 pipefail | 12 | 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 ]; then | 81 | + | 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" >&2 | 82 | echo "no frame presented in ${secs}s: the page did not animate or neferwl stalled" >&2 | ||
| 76 | tail -n 20 "$tmp/nwl.out" >&2 | 83 | tail -n 20 "$tmp/nwl.out" >&2 | ||
| 77 | exit 1 | 84 | exit 1 | ||
| 78 | fi | 85 | 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 sys | 88 | 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 * 100 | 95 | 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 | EOF | 98 | EOF | ||
| 87 | 99 | ||||
| 88 | if [ -n "${BENCH_KEEP:-}" ]; then | 100 | 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> | |||