Measure the stream's pause before the forced garbage collection
feuerwerk / gate (push) Successful in 4m38s
feuerwerk / suite (push) Successful in 18m39s

e2e 34 "the pause holds the stream" was red in CI tasks 712, 719 and
721. The pause was not at fault. Right before it the step forces a
full garbage collection for its memory check. On the CI runner that
held the page for seconds, and the player ran dry before the pause was
even clicked: 0.02 s and 0.05 s were left ahead in 719 and 721, against
about 2 s in the green CI runs and 4 s on melektaus-atx.

- The worklet counts a late sample only once it arrives and reports
  every 4800 samples read. So the underrun from the collection showed
  up after the resume and looked like samples the pause had lost.
- The pause is now measured first, on a stream held at least a second
  ahead, with half a second for the worklet to report. The collection
  and the renderer's size follow while the game still plays.
- The check judges what the pause adds: as many skipped samples after
  the resume as before the pause.

Tests:
- e2e 34 on melektaus-atx with a 5 s busy loop on the page where the
  collection was: 12375 samples skipped before the pause, the same
  after it. The old condition, none skipped, fails on these values.
  The new one passes.
  ALL PASSED, 148 checks
- Lint and Prettier on the module are clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-18 13:13:00 +02:00
1 parent 97e0d93168
commit f296f427ef
1 file changed
+14 -4
+14 -4
View File
@@ -21,6 +21,8 @@ import { sampleZoom } from './lib/zoomTransition.mjs';
const LANE = LANES.sound;
const BLIP_BYTES = 4044;
const DRONE_BYTES = 12044;
/** A second of one channel at the stream rate, as the library counts the samples it holds (#349). */
const STREAM_SECOND_BYTES = 48000 * Float32Array.BYTES_PER_ELEMENT;
export async function sound(t) {
const { check, sleep, json, evaluate, mutate, click, clickText, press, play, stop, shot, showTab } = t;
@@ -3956,9 +3958,14 @@ export async function sound(t) {
(peaks) => Math.min(Math.max(...(peaks.mix ?? [0])), Math.max(...(peaks['mix:sn-bus'] ?? [0]))) > 0.01,
);
await sleep(1000);
// After a garbage collection as at rest: play mode makes garbage of its own (#378, CI task 614).
const playingStreamed = await restingMb();
const renderersPlaying = renderers();
// The pause is measured on a stream held a second ahead, and before the garbage collection below:
// forced on the CI runner, that held the page for seconds and the player ran dry before the pause
// was clicked, 0.02 s left ahead (CI tasks 712, 719, 721). A sample that came too late is only
// counted once it is there, so the pause is judged by what it adds. The wait after it lets the
// worklet report how far it has read: until then a stretch sent counts as ahead.
const headsOnly = statsBefore.streaming.held;
await waitFor(stats, (s) => s.streaming.held - headsOnly >= STREAM_SECOND_BYTES, 40);
await sleep(500);
const heldStreamed = await stats();
await t.clickToolbar('pause');
await sleep(500);
@@ -3968,6 +3975,9 @@ export async function sound(t) {
await t.clickToolbar('pause');
await sleep(1000);
const resumedLong = await stats();
// After a garbage collection as at rest: play mode makes garbage of its own (#378, CI task 614).
const playingStreamed = await restingMb();
const renderersPlaying = renderers();
await stop();
const stoppedLong = await stats();
check(
@@ -3983,7 +3993,7 @@ export async function sound(t) {
paused.state === 'suspended' &&
pausedLater === paused.clock &&
resumedLong.streaming.playing === 1 &&
resumedLong.streaming.skipped === 0 &&
resumedLong.streaming.skipped === heldStreamed.streaming.skipped &&
stoppedLong.streaming.playing === 0 &&
stoppedLong.playing === 0,
// Held before the pause too: it tells a stream that fell behind from the start from one the