Auto-throttle visualizer under sustained Opus decode pressure; strip streaming investigation instrumentation

This commit is contained in:
daniel-c-harvey
2026-06-26 06:00:05 -04:00
parent 76f7f389a3
commit 374f09150f
7 changed files with 313 additions and 159 deletions
@@ -44,6 +44,8 @@
* position while !isPlaying). The loop stops only on tab-hidden (visibilitychange) and dispose.
*/
import { decodePressure } from '../audio/decodePressure.js';
// ── Tuning anchors (see spec §B). These are the load-bearing constants. ──────────
/**
@@ -148,6 +150,16 @@ const RIBBON_HALF_WIDTH_FRAC = 0.92;
*/
const MAX_DPR = 2;
/**
* Minimum milliseconds between drawn frames WHILE decode is under sustained pressure (Part 1 —
* auto-protect audio). 1000/15 ≈ 66.7 ms caps the loop at ~15 fps, cutting the main-thread WebGL
* software-render + physics cost by ~75% so the synchronous WebCodecs Opus decode (which shares the
* main thread when HW accel is off) gets the time it needs to keep up. The decodePressure signal is
* false in the common case (HW accel on, or lossless), so this cap never applies and the loop draws
* every frame at full quality. Tunable; the exact fps that clears starvation is browser-confirmed.
*/
const PRESSURE_THROTTLE_FRAME_MS = 1000 / 15;
// ════════════════════════════════════════════════════════════════════════════════════
// R2 — the wax-blob lava physics (CPU step + uniform upload). The lava is now a real
// Lagrangian particle system integrated each frame on the JS side and rendered as
@@ -1679,6 +1691,10 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
let rafId: number | null = null;
let disposed = false;
const startTimeMs = performance.now();
// Wall-clock of the last DRAWN continuous-loop frame, for the decode-pressure throttle (Part 1).
// While decodePressure.isUnderPressure() the loop draws at most once per PRESSURE_THROTTLE_FRAME_MS
// so the main thread yields time back to a starved decode; unthrottled it draws every frame.
let lastDrawMs = performance.now();
// Wall-clock anchor for the physics dt (separate from the playhead decay clock).
let lastPhysicsMs = performance.now();
@@ -1923,9 +1939,30 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
rafId = null;
return;
}
// Auto-protect audio under decode pressure (Part 1). When the WebCodecs Opus decode pipeline
// reports SUSTAINED lag (decodePressure.isUnderPressure()), throttle the draw cadence to
// ~PRESSURE_THROTTLE_FRAME_MS so this loop's main-thread GL + physics cost yields time back to
// decode; we still reschedule every frame so full cadence resumes the instant decode recovers.
// A no-op when decode is healthy — isUnderPressure() stays false, the gate is always open, and
// every frame draws exactly as before. Skipping a draw also skips the physics step (it runs
// inside draw()), and its dt is clamped to PHYSICS_MAX_DT, so a throttled gap never lurches the
// lava. redrawOnce() (idle/control-tweak stills) is intentionally NOT throttled — those are rare
// one-shots, not the continuous loop.
const nowMs = performance.now();
if (!decodePressure.isUnderPressure() || nowMs - lastDrawMs >= PRESSURE_THROTTLE_FRAME_MS) {
lastDrawMs = nowMs;
drawFrame();
}
rafId = requestAnimationFrame(frame);
}
/** One drawn continuous-loop frame: the GL draw plus the gated FPS/lava diagnostic tally. */
function drawFrame(): void {
draw();
// FPS tally: count this callback, and once per elapsed second emit the rate.
// FPS tally: count this drawn frame, and once per elapsed second emit the rate.
// performance.now() is cheap (no GPU stall, unlike gl.getError); the gated log
// fires at most once/sec, so this adds no meaningful per-frame cost.
if (DEBUG) {
@@ -1968,10 +2005,6 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
fpsWindowStartMs = nowMs;
}
}
// Reschedule unconditionally — the loop runs continuously now (lava reframe Part C); it is
// stopped only by dispose() or the tab going hidden, never by audio pausing.
rafId = requestAnimationFrame(frame);
}
// ── Tab-visibility gating (lava reframe Part C power-saving). ────────────────────