Skip to content

Scope the late-start test to what it actually measures - #89

Open
chrisuthe wants to merge 3 commits into
masterfrom
chrisuthe/task/scope-the-late-start-test-to-one-start
Open

chrisuthe wants to merge 3 commits into
masterfrom
chrisuthe/task/scope-the-late-start-test-to-one-start

Conversation

@chrisuthe

Copy link
Copy Markdown
Owner

Correcting an overclaim I merged in #88, before it gets read as coverage.

The defect

LateStart_WithDeclaredOutputLatency_StaysOnSchedule named a property of the general case while measuring a single start. RunDriftFreeSession builds one TimedAudioBuffer and starts playback once, so nothing in the harness observes what repeated starts do. The name was true of start one and silent about start five.

Renamed to LateStart_PreservesTheOutputLatencyPreRoll — the claim the assertion actually supports: SkipStaleAudio's re-derivation runs ScheduledLocalTimeFor a second time against a head cursor that has already moved, and does not lose or double-count the pre-roll. That is worth pinning. It is just not the same statement.

Why it matters

A capture rig running this player and the C++ CLI into one timebase — both in one MA sync group, each to its own null sink, both captured by a single ffmpeg process — measured the .NET side displacing at every stream restart and never recovering.

total move, 22 restarts −525.1 ms, monotonic across 19 of 19 bins
overall slope −350.3 ms/hour
mean within-interval slope −16.5 ms/hour
share explained by continuous drift 5%

95% of the displacement happens at the restarts themselves. The control is what makes this a defect rather than restart noise: two C++ instances under identical conditions were also perturbed at each restart, but with no consistent direction — 4 of 10 negative, mean +6.4 ms — so they random-walk and stay bounded, while the .NET leg moved the same way 16 times out of 22.

A test asserting "late starts stay on schedule" would have been read as covering precisely the case that is broken. That is worse than having no test there, which is the whole reason #87 exists.

What the remark now records

The gap, and what closing it needs: repeated starts against one timebase, asserting that displacement after N restarts is not N times worse than after one. It belongs to the SDK's shared startup path rather than to this repository — it reproduces on Linux/OpenAL, so it is not WASAPI and not windowsSpin.

Deliberately not asserted

Why each restart displaces. The absorbed startup baseline is near-constant at −23 ms but does not predict the per-restart step: r = −0.113 over 23 pairs, 7 of them positive, steps ranging +29 to −111 ms. The aggregate agreement between "−23.9 ms mean displacement" and "−23 ms mean baseline" is two means coinciding, not a mechanism — I relayed it as one earlier and that was wrong.

AbsorbedStartupBaseline_IsNotMisalignment is untouched and stands: it asserts the absorbed baseline is not a measure of misalignment, which the per-event pairing independently confirms from real audio.

Testing

dotnet test -c Release on Windows: 219 passed, 0 failed.

Credit for the measurements and the C++/C++ control to the Linux capture rig; the analysis of what the test does and does not cover is from reading the harness.

LateStart_WithDeclaredOutputLatency_StaysOnSchedule named a property of the
general case while measuring a single start. RunDriftFreeSession builds one
TimedAudioBuffer and starts playback once, so nothing in the harness observes
what repeated starts do - the name was true of start one and said nothing about
start five. Renamed to LateStart_PreservesTheOutputLatencyPreRoll, which is the
claim the assertion supports: the SkipStaleAudio re-derivation runs
ScheduledLocalTimeFor a second time against a head cursor that has already
moved, and does not lose or double-count the pre-roll.

The distinction turned out to matter. A capture rig running this player and the
C++ CLI into one timebase measured the .NET side displacing at every stream
restart and never recovering: mean ~24ms per restart, 95% of it at the restart
rather than between them (overall -350.3 ms/hour against a mean within-interval
slope of -16.5 ms/hour), cumulative and permanent - -525ms over 22 restarts,
monotonic across 19 of 19 bins. Two C++ instances under identical conditions
were also perturbed at restarts but with no consistent direction, 4 of 10
negative, so they random-walk and stay bounded. That control is what makes it a
defect rather than restart noise.

So a test asserting late starts stay on schedule would have been read as
covering exactly the case that is broken, which is worse than having no test
there at all. The remark now records the gap and what closing it needs -
repeated starts against one timebase, asserting displacement after N restarts is
not N times worse than after one - and notes it belongs to the SDK's shared
startup path, since it reproduces on Linux/OpenAL.

Not asserted anywhere, because it is not established: why each restart displaces.
The absorbed startup baseline sits near-constant at -23ms but does not predict
the per-restart step (r = -0.113 over 23 pairs, 7 of them positive, steps
ranging +29 to -111ms). The aggregate agreement between the two figures is two
means coinciding, not a mechanism.
The stale-audio assertion holds for every case including startLateMicros: 0, so
SkipStaleAudio runs on every start here - declaring an output latency pre-rolls
the schedule past the grace window, and the harness cannot construct an ordinary
start at all.

That is worth stating next to the coverage gap, because the rare path is the
only one modelled. In the measured run the branch ran once in 29 starts; the
other 28 stayed inside the grace window, and those are what the -525ms
accumulated across. The stale branch does explain the outsized residuals -88.9ms
on that single start against -21.9 to -23.7ms on ordinary ones - but a larger
absorption is a difference in what gets absorbed, not in what accumulates.

So the displacement is something an ordinary stream start does, and this file
cannot reach an ordinary stream start. Recorded rather than fixed: making the
harness model one means a start inside the grace window with the pre-roll
declared, which is a different harness rather than another case.
The remark had started tracking an investigation rather than describing the
test, and had already needed one correction: it said the player displaces at
every stream restart, which turned out to be restart-kind dependent. Seeking
barely registers; stopping and restarting is what displaces.

Restructured so what stays is what can be re-checked from this file - the
harness starts playback once, and SkipStaleAudio runs on every case including
startLateMicros: 0, so it cannot construct an ordinary start. Both are
properties of the harness, not of the code under test, and neither depends on
anyone's measurements.

Kept two external facts because they decide whether a reproducer works at all:
it is the stop/start cycle rather than seeking, and it reproduces on
Linux/OpenAL so it is the shared SDK startup path. The rest of the diagnosis now
points at sendspin-dotnet#272 instead of being restated here. Four candidate
mechanisms have looked settled and then failed a disconfirming check - the
resampler ratio, the absorbed baseline, the stale-start branch, the restart
count - and a comment that cannot be re-run is the wrong place to keep score.

Also recorded the corroboration on AbsorbedStartupBaseline_IsNotMisalignment,
which is now the best-supported claim in the file: three hardware measurements
from different directions agree that the absorbed value stays put while the
displacement varies. Measurements rather than mechanisms, so they are worth
writing down.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant