ci(e2e): give the container an audio device that keeps time
The e2e job's red history is one measurement being wrong. ALSA's `null` plugin does not pace: measured in this exact image through beep and oto with the same speaker.Init arguments player.InitSpeaker uses, 3000 ms of audio is consumed in 2.96 ms — a thousand times too fast. So every track finished instantly, the position reset to zero, and three specs failed on a clock that never moved. It read as a flake because InitSpeaker succeeds either way, in ~3 ms either way. A PulseAudio null sink is timer-scheduled: the same 3000 ms takes 3762 ms, and 12 s takes 13.5 s — the overhead is a constant buffer drain, not a rate error. Verified under the private session bus and Xvfb dev-headless.sh runs the app in, with no system D-Bus and no kernel module, which is what makes it reachable from a container. The sink is a dependency with a rate, so it is now checked like one: a step plays three seconds and fails if they take under two. Without it the failure surfaces three steps later as "the elapsed clock is 19 s adrift", which reads as an app bug and cost two sessions of exactly that suspicion.
This commit is contained in:
+59
-11
@@ -208,7 +208,8 @@ jobs:
|
|||||||
apt-get install -y -qq --no-install-recommends \
|
apt-get install -y -qq --no-install-recommends \
|
||||||
ca-certificates curl git jq build-essential pkg-config \
|
ca-certificates curl git jq build-essential pkg-config \
|
||||||
libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev \
|
libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev \
|
||||||
xvfb dbus dbus-x11 ffmpeg libasound2t64
|
xvfb dbus dbus-x11 ffmpeg libasound2t64 \
|
||||||
|
alsa-utils libasound2-plugins pulseaudio pulseaudio-utils
|
||||||
|
|
||||||
- name: Clone repo at this commit
|
- name: Clone repo at this commit
|
||||||
run: |
|
run: |
|
||||||
@@ -254,17 +255,64 @@ jobs:
|
|||||||
playwright-cli install-browser chromium
|
playwright-cli install-browser chromium
|
||||||
|
|
||||||
# oto/v3 talks to libasound directly, and a container has no
|
# oto/v3 talks to libasound directly, and a container has no
|
||||||
# PulseAudio socket to fall back on. ALSA's null plugin advances
|
# PulseAudio socket to fall back on — so it needs a default device
|
||||||
# its pointer on a timer rather than discarding instantly, so beep
|
# that not only accepts audio but **paces** it, because the player's
|
||||||
# is consumed at real-time rate and the elapsed clock actually
|
# position is derived from what has been consumed.
|
||||||
# moves — which playback.spec.ts asserts. Measured: InitSpeaker
|
#
|
||||||
# succeeds in ~36 ms and all six playback specs pass. Without
|
# ALSA's `null` plugin does not pace. It was used here on the
|
||||||
# this, app.go joins the failure into startupErr and everything
|
# belief that it advances its pointer on a timer. Measured in
|
||||||
# except playback still works, so the suite fails looking like
|
# this exact image, through beep and oto with the same
|
||||||
# flake rather than like a missing dependency.
|
# `speaker.Init` arguments `player.InitSpeaker` uses:
|
||||||
- name: Null audio sink
|
#
|
||||||
|
# type null 3000 ms of audio consumed in 2.96 ms
|
||||||
|
# pulse sink 3000 ms of audio consumed in 3762 ms
|
||||||
|
#
|
||||||
|
# A thousand times too fast. Every track finished instantly, the
|
||||||
|
# position reset to zero, and three specs failed on a clock that
|
||||||
|
# never moved — which is the whole of the e2e job's red history,
|
||||||
|
# and it looked like a flake because `InitSpeaker` succeeds either
|
||||||
|
# way (in ~3 ms, also either way).
|
||||||
|
#
|
||||||
|
# PulseAudio's null sink is timer-scheduled and does pace — the
|
||||||
|
# 0.76 s over is the buffer draining, not a rate error; 12 s of
|
||||||
|
# audio takes 13.5 s. Verified under the private session bus and
|
||||||
|
# Xvfb that dev-headless.sh runs the app in. It needs no system
|
||||||
|
# D-Bus and no kernel module, which is why it is reachable from a
|
||||||
|
# container at all.
|
||||||
|
- name: Real-time audio sink
|
||||||
run: |
|
run: |
|
||||||
printf 'pcm.!default { type null }\nctl.!default { type null }\n' > /etc/asound.conf
|
set -eu
|
||||||
|
# --system because the job runs as root and PulseAudio refuses
|
||||||
|
# to start as root any other way.
|
||||||
|
adduser root pulse-access
|
||||||
|
pulseaudio --system --daemonize --disallow-exit \
|
||||||
|
--exit-idle-time=-1 \
|
||||||
|
--load="module-null-sink sink_name=yellowjacket"
|
||||||
|
printf 'pcm.!default { type pulse }\nctl.!default { type pulse }\n' \
|
||||||
|
> /etc/asound.conf
|
||||||
|
pactl list short sinks
|
||||||
|
|
||||||
|
# The sink is a dependency with a *rate*, so it is checked like
|
||||||
|
# one. Without this the failure surfaces three steps later as
|
||||||
|
# "the elapsed clock is 19 s adrift", which reads as an app bug
|
||||||
|
# and cost two sessions of exactly that suspicion.
|
||||||
|
- name: The sink plays at real time
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
ffmpeg -loglevel quiet -f lavfi -i "sine=frequency=440:duration=3" \
|
||||||
|
-ar 44100 /tmp/probe.wav
|
||||||
|
# aplay rather than the app: this is a check on the *device*,
|
||||||
|
# and it has to be able to fail before the app is built.
|
||||||
|
start=$(date +%s%N)
|
||||||
|
aplay -q /tmp/probe.wav
|
||||||
|
ms=$(( ($(date +%s%N) - start) / 1000000 ))
|
||||||
|
echo "3000 ms of audio took ${ms} ms"
|
||||||
|
if [ "$ms" -lt 2000 ]; then
|
||||||
|
echo "The default ALSA device is discarding audio rather than" \
|
||||||
|
"playing it. Every track will finish instantly and the" \
|
||||||
|
"player's position will never advance." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Fixtures and seed
|
- name: Fixtures and seed
|
||||||
working-directory: /src
|
working-directory: /src
|
||||||
|
|||||||
Reference in New Issue
Block a user