From 8170f71e42e9aa1fea231d2309c83921c998424c Mon Sep 17 00:00:00 2001 From: Aqua Date: Thu, 5 Nov 2026 19:05:15 +0000 Subject: [PATCH] web: bench fixes (done flag, readback sync), runner kills browser on failure, BENCH.md Co-Authored-By: Claude Sonnet 5.5 --- web/BENCH.md | 13 +++++++++++++ web/README.md | 2 +- web/src/main.js | 5 +++-- web/tools/headless.mjs | 2 ++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 web/BENCH.md diff --git a/web/BENCH.md b/web/BENCH.md new file mode 100644 index 0000000..8ebe61b --- /dev/null +++ b/web/BENCH.md @@ -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. diff --git a/web/README.md b/web/README.md index 048f40d..a9b988e 100644 --- a/web/README.md +++ b/web/README.md @@ -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 diff --git a/web/src/main.js b/web/src/main.js index 252876a..747faed 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -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); } diff --git a/web/tools/headless.mjs b/web/tools/headless.mjs index 50fe35d..b005939 100644 --- a/web/tools/headless.mjs +++ b/web/tools/headless.mjs @@ -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;