From f296f427ef09d0fb15b94dfbffc4b83fb1d38e96 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sun, 18 Oct 2026 13:13:00 +0200 Subject: [PATCH] Measure the stream's pause before the forced garbage collection 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 --- tools/e2e/34-sound.mjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tools/e2e/34-sound.mjs b/tools/e2e/34-sound.mjs index 56fdde14..4fb3a695 100644 --- a/tools/e2e/34-sound.mjs +++ b/tools/e2e/34-sound.mjs @@ -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