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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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> | |||