From 87ec62aace3af40b0b72cb2773df5845a5f59e02 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Fri, 9 Oct 2026 09:51:15 +0200 Subject: [PATCH] Keep the scheduler check in e2e 34 from flaking on a clock tick (#366) The check "the scheduler plans a looped bar" read the position and the audio clock one after the other and compared them to 1 tick. In Chrome the audio clock moves on by a render block even inside one task, so now and then the two reads lay 2.9 ms (3.5 ticks) apart. It was red in CI task 580 and in 2 of 5 runs during #351. - The clock is now read before and after the position. The position has to lie between the two, with 1 tick to spare, also across the seam of the loop. - The scheduler itself is unchanged, it was never wrong. Tests: - e2e 34 (melektaus-atx): ALL PASSED, 93 checks, 3 of 3 runs. - Counter-probe with the position 10 ticks off: this check red. - Only the e2e file changed. node --check, Prettier and ESLint are clean on it, the gate did not run again. 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 33bf7944..1c43a9d7 100644 --- a/tools/e2e/34-sound.mjs +++ b/tools/e2e/34-sound.mjs @@ -1714,6 +1714,8 @@ export async function sound(t) { // quarters at 150 BPM in a loop: every note is planned before its second comes, each 0.4 s // after the one before, also across the seam into the next round. Where the music stands // is the audio clock less the output latency, counted from the first note. + // #366: the audio clock moves on by a render block even between two reads in one expression, so + // it is read before and after the position, and the position lies between the two. const bar = [60, 62, 64, 65].map((pitch, i) => ({ tick: i * 480, length: 240, @@ -1730,13 +1732,21 @@ export async function sound(t) { 40, ); const where = await json( - '({ position: __gm.music.position(), clock: __gm.sound.clock(), latency: __gm.music.latency() })', + `({ + before: __gm.sound.clock(), + position: __gm.music.position(), + after: __gm.sound.clock(), + latency: __gm.music.latency(), + })`, ); await evaluate('__gm.music.stop()'); const musicOff = await waitFor(stats, (s) => s.playing === 0); const apart = planned.slice(1, 9).map((p, i) => +(p.at - planned[i].at).toFixed(6)); const ahead = planned.map((p) => +(p.at - p.planned).toFixed(3)); - const heardTicks = (((where.clock - where.latency - planned[0].at) * 150 * 480) / 60) % 1920; + const ticksHeardAt = (clock) => ((clock - where.latency - planned[0].at) * 150 * 480) / 60; + const heardTicks = [ticksHeardAt(where.before), ticksHeardAt(where.after)]; + // How far the position lies past the first read, around the loop of 1920 ticks. + const pastBefore = (((where.position.ticks - heardTicks[0]) % 1920) + 1920) % 1920; check( 'the scheduler plans a looped bar on the audio clock: every note ahead of its second, 0.4 s apart across the seam', planned.length >= 9 && @@ -1747,11 +1757,11 @@ export async function sound(t) { planned.every((p) => p.voice > 0 && Math.abs(p.duration - 0.2) < 1e-9 && p.tempo === 150) && apart.every((gap) => Math.abs(gap - 0.4) < 1e-6) && ahead.every((s) => s > 0 && s <= 0.16) && - Math.abs(where.position.ticks - heardTicks) < 1 && + (pastBefore <= heardTicks[1] - heardTicks[0] + 1 || pastBefore > 1919) && where.position.bar === 1 && musicOff.playing === 0 && (await json('__gm.music.playing()')) === false, - JSON.stringify({ apart, ahead, where, heardTicks, playing: musicOff.playing }), + JSON.stringify({ apart, ahead, where, heardTicks, pastBefore, playing: musicOff.playing }), ); // A note planned ahead through the pitch worklet starts on its sample, also inside a render block: