web: bench fixes (done flag, readback sync), runner kills browser on failure, BENCH.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-05 19:05:15 +00:00
commit 8170f71e42
4 changed files with 19 additions and 3 deletions

13
web/BENCH.md Normal file
View file

@ -0,0 +1,13 @@
# Frame-time record
**No GPU measurement exists yet.** The only run so far used headless Chrome with the SwiftShader *software* rasterizer on a shared Linux box. Those numbers say nothing about a real GPU; they show that the scenes run without GL errors and what the JavaScript side costs. The intended target machine (Starfire, GTX 1070) was unreachable (sshd reset every connection) when this was written.
Run 2026-11-05, 1280x720 drawing buffer, synthetic feed, `?bench&sync&w=1280&h=720&ms=4000` (a 1-pixel readback each frame, so the number is cost, not vsync-capped). 16.7 ms is the 60 Hz budget.
| Scene | frame p50 | p95 | max | GPU timer p50 (software) |
|---|---|---|---|---|
| Aurora disk | 21.3 ms | 27.1 | 38.6 | 17.9 |
| Burning water | 46.1 ms | 52.6 | 80.7 | 42.5 |
| Vector trails | 12.2 ms | 17.9 | 21.4 | 9.2 |
JavaScript submit cost (analysis, uploads, draw calls) was 0.8 ms p50 and under 2.7 ms p95 per frame in the vsync run. Software rasterization is CPU-bound by pixels and vertices, so the water scene (full-screen 3 x fbm per pixel) is the one to watch on a real GPU, particularly at 4K or high-dpi. Run `node web/tools/serve.mjs` and open `/?bench&sync&w=1920&h=1080` in Chrome, or use `web/tools/headless.mjs`, on the target to replace this table.

View file

@ -37,7 +37,7 @@ cd web && npm test # node --test test/dsp.test.mjs (node >= 20, nothing t
Covers: ten synthetic clicks found at two hop sizes (11.6 ms and 16.7 ms) with no false positives; silence, steady noise and a sustained tone give no onsets; a 20 ms doublet merges under the refractory period; the key estimate reads C major, A minor and (transposed) B minor from synthesized chord progressions. Fixtures are deliberately plain: the estimate is known to confuse relative major/minor when a clip leans on the dominant, and one test fixture was rewritten to put its tonic in more chords (the estimator was not tuned to the fixture).
Frame time: the HUD shows rolling p50/p95 of requestAnimationFrame intervals against the 16.7 ms budget and CPU submit time. `?bench` runs the three scenes on the synthetic feed and prints JSON (`?w=1920&h=1080` fixes the drawing-buffer size, `&sync` times each frame with `gl.finish()` so vsync does not hide cost, `&ms=` sets seconds per scene). `web/tools/headless.mjs` drives headless Chrome over CDP to collect it. Results of the last run are in `web/BENCH.md`.
Frame time: the HUD shows rolling p50/p95 of requestAnimationFrame intervals against the 16.7 ms budget and CPU submit time. `?bench` runs the three scenes on the synthetic feed and prints JSON (`?w=1920&h=1080` fixes the drawing-buffer size, `&sync` reads a pixel back after each frame so the measured frame time is real cost, not capped by vsync, `&ms=` sets seconds per scene). `web/tools/headless.mjs` drives headless Chrome over CDP to collect it. Results of the last run are in `web/BENCH.md`.
## Provenance

View file

@ -79,7 +79,7 @@ function pct(a, n, p) { const s = Array.from(a.subarray(0, n)).sort((x, y) => x
const record = { scenes: [] };
let benchScene = -1, benchStart = 0, benchDt = [], benchCpu = [], benchGpu = [];
const BENCH_MS = +(q.get('ms') || 6000), WARM_MS = 1000;
const sync = q.has('sync');
const sync = q.has('sync'), syncPx = new Uint8Array(4);
function frame(now) {
const t0 = performance.now();
@ -101,7 +101,7 @@ function frame(now) {
sc.draw(gl, A, canvas.width, canvas.height, hist);
fb.present();
if (qy) { gl.endQuery(timer.TIME_ELAPSED_EXT); pendingQ.push(qy); }
if (sync) gl.finish();
if (sync) gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, syncPx); // gl.finish() does not block under ANGLE; reading a pixel does
cpuMs = performance.now() - t0;
const dt = last ? now - last : 16.7; last = now;
@ -131,6 +131,7 @@ function frame(now) {
}
function stepBench(now, dt, cpu) {
if (benchScene === 99) return;
if (benchScene < 0) { benchScene = 0; benchStart = now; setScene(0); }
const el = now - benchStart;
if (el > WARM_MS) { benchDt.push(sync ? cpu : dt); benchCpu.push(cpu); }

View file

@ -14,6 +14,8 @@ const proc = spawn(chrome, [`--remote-debugging-port=${port}`, `--user-data-dir=
'--autoplay-policy=no-user-gesture-required', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--window-size=1280,720',
...(process.env.CHROME_FLAGS ? process.env.CHROME_FLAGS.split(' ') : []), 'about:blank'], { stdio: 'ignore' });
const stop = code => { try { proc.kill(); } catch {} process.exit(code); };
process.on('uncaughtException', e => { console.error(e); stop(1); }); // never leave the browser running if we die first
process.on('exit', () => { try { proc.kill(); } catch {} });
const sleep = ms => new Promise(r => setTimeout(r, ms));
let targets;