Auto-throttle visualizer under sustained Opus decode pressure; strip streaming investigation instrumentation
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
|
||||
import { AudioContextManager } from './AudioContextManager.js';
|
||||
import { decodePressure } from './decodePressure.js';
|
||||
|
||||
/**
|
||||
* Provisional back-retain default. The window-size POLICY (OQ1/OQ3) is not decided yet, so
|
||||
@@ -58,15 +59,17 @@ const DEFAULT_BACK_RETAIN_SECONDS = 10;
|
||||
* footprint is tiny (48 kHz stereo ≈ 0.37 MB/s, so 60 s ≈ 23 MB — a fraction of the 96 MB cap)
|
||||
* yet whose per-packet decode jitter (HW-accel-off software decode, main-thread AudioData copies)
|
||||
* needs a deeper buffer to stay ahead of the playhead. Doubling the window lets Opus use the memory
|
||||
* headroom the byte cap already permits. The byte cap is UNCHANGED, so high-density formats
|
||||
* (lossless) still pause at exactly the same footprint as before — the OOM fix does not regress.
|
||||
* headroom the byte cap already permits. The byte cap is UNCHANGED, so a high-footprint stream
|
||||
* still pauses at exactly the same footprint as before — the OOM fix does not regress.
|
||||
*
|
||||
* OQ3 hard memory ceiling: an absolute byte cap on total decoded float held, independent of the
|
||||
* time window. This is the guard-rail that makes "1 GB never OOMs" a guarantee rather than a
|
||||
* tuning hope — production pauses on `lookahead >= high OR bytes > cap`, whichever fires first, so
|
||||
* the footprint can never exceed the cap regardless of the time window. For dense lossless the
|
||||
* byte cap fires before 60 s (bounding memory exactly as the old 30 s window's byte estimate did);
|
||||
* for sparse Opus the time window fires first, at ~23 MB. Estimated as channels × frames × 4 (f32).
|
||||
* the footprint can never exceed the cap regardless of the time window. The decoded f32 footprint
|
||||
* scales with sample rate × channels (not source codec), so for high-sample-rate / multichannel
|
||||
* audio the byte cap fires before 60 s (bounding memory exactly as the old 30 s window's byte
|
||||
* estimate did); for sparse 48 kHz stereo Opus the time window fires first, at ~23 MB. Estimated
|
||||
* as channels × frames × 4 (f32).
|
||||
*/
|
||||
const DEFAULT_FORWARD_HIGH_WATER_SECONDS = 60;
|
||||
const DEFAULT_FORWARD_LOW_WATER_SECONDS = 30;
|
||||
@@ -83,7 +86,7 @@ const BYTES_PER_FLOAT_SAMPLE = 4;
|
||||
* resumed on the next ~20 ms, and so on — the audible start/stop thrash during the WebCodecs decode
|
||||
* ramp. Gating on a fixed LEAD in seconds gives a resume the same cushion a fresh start has,
|
||||
* independent of format. 1 s is the same order as the lossless playback-start lead (~6 segments) and
|
||||
* sits far below the 30 s forward high-water, so back-pressure never throttles production while the
|
||||
* sits far below the 60 s forward high-water, so back-pressure never throttles production while the
|
||||
* scheduler is still re-accumulating this lead. Tunable; not magic.
|
||||
*/
|
||||
const DEFAULT_MIN_PLAYBACK_LEAD_SECONDS = 1.0;
|
||||
@@ -305,7 +308,6 @@ export class PlaybackScheduler {
|
||||
evaluateProductionPause(): boolean {
|
||||
const lookahead = this.getForwardLookaheadSeconds();
|
||||
const overByteCeiling = this.maxDecodedBytes > 0 && this.getDecodedByteEstimate() > this.maxDecodedBytes;
|
||||
const wasPaused = this.productionPaused_;
|
||||
|
||||
if (this.productionPaused_) {
|
||||
// Stay paused until BOTH the time window has drained below low-water AND the byte
|
||||
@@ -317,19 +319,6 @@ export class PlaybackScheduler {
|
||||
this.productionPaused_ = true;
|
||||
}
|
||||
|
||||
// [BP-DIAG] Log only the latch TRANSITIONS (not per-call) so a browser run shows exactly when
|
||||
// production was throttled and the live numbers at that instant — the test for "production
|
||||
// paused while decoded audio is actually low" (the prime block hypothesis). If a PAUSED line
|
||||
// ever shows a small lookahead, the lookahead computation is the culprit; if it always shows
|
||||
// ~high-water, back-pressure is innocent and the symptom is decode throughput. Trivially removable.
|
||||
if (wasPaused !== this.productionPaused_) {
|
||||
console.log(
|
||||
`[BP-DIAG] production ${this.productionPaused_ ? 'PAUSED' : 'RESUMED'} ` +
|
||||
`lookahead=${lookahead.toFixed(2)}s bytes=${(this.getDecodedByteEstimate() / 1048576).toFixed(1)}MB ` +
|
||||
`buffers=${this.buffers.length} nextIdx=${this.nextBufferIndex} ` +
|
||||
`pos=${this.getCurrentPosition().toFixed(2)}s overByteCeiling=${overByteCeiling}`);
|
||||
}
|
||||
|
||||
return this.productionPaused_;
|
||||
}
|
||||
|
||||
@@ -500,12 +489,6 @@ export class PlaybackScheduler {
|
||||
if (!this.streamComplete && !this.hasMinimumPlaybackLead()) {
|
||||
return; // still re-accumulating the rebuffer lead — remain parked
|
||||
}
|
||||
// [BP-DIAG] Underrun resume — the playhead drained mid-stream and we have now rebuilt the
|
||||
// lead. Frequent RESUME lines (paired with the PARK lines below) are the "repeatedly hits end
|
||||
// of buffer" thrash: decode is not staying ahead. Trivially removable.
|
||||
console.log(
|
||||
`[BP-DIAG] underrun RESUME lead=${this.getForwardLookaheadSeconds().toFixed(2)}s ` +
|
||||
`buffers=${this.buffers.length} nextIdx=${this.nextBufferIndex} streamComplete=${this.streamComplete}`);
|
||||
this.underrun_ = false;
|
||||
this.isActive_ = true;
|
||||
this.playbackAnchorTime = this.contextManager.currentTime;
|
||||
@@ -610,12 +593,11 @@ export class PlaybackScheduler {
|
||||
this.finishPlayback();
|
||||
} else {
|
||||
this.underrun_ = true;
|
||||
// [BP-DIAG] Mid-stream underrun: the scheduled queue drained and decode has not caught up.
|
||||
// This is the symptom Daniel reports. The paired RESUME line above shows how long the gap
|
||||
// lasted and what lead it rebuilt to. Trivially removable.
|
||||
console.log(
|
||||
`[BP-DIAG] underrun PARK pos=${this.getCurrentPosition().toFixed(2)}s ` +
|
||||
`buffers=${this.buffers.length} nextIdx=${this.nextBufferIndex}`);
|
||||
// Mid-stream underrun: the scheduled queue drained and decode has not caught up. Report it
|
||||
// as decode pressure so the visualizer throttles — a sustained run of these is exactly the
|
||||
// HW-accel-off starvation the auto-throttle protects against. The hysteresis in the signal
|
||||
// ignores a lone startup-ramp underrun; only a sustained run engages the throttle.
|
||||
decodePressure.report();
|
||||
// Hold the playhead at the decoded tail so getCurrentPosition stays exact during
|
||||
// the gap. isActive_ goes false so no stale-anchor scheduling occurs; resume
|
||||
// re-anchors at currentTime when buffers arrive.
|
||||
|
||||
Reference in New Issue
Block a user