From b75eb1dcfb576d93931afedc8062105d258fca5f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 13 Oct 2026 12:03:12 -0700 Subject: [PATCH 1/2] quest(m1): plan failing an export on a source it cannot follow Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/slow-source.md | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 quest/m1/slow-source.md diff --git a/quest/m1/README.md b/quest/m1/README.md index fb2da76e2ca..e49a32b3ab8 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -76,6 +76,7 @@ above them, since the epoch anchor requires it. - [Live largest location](/quest/m1/largest.md) - `largest()` replaces `latest()` in Rust and JS as a live stream of the track's largest location, known before the first group from the publisher's report - [Untimed lite-07](/quest/m1/lite-untimed.md) - lite-07 carries an untimed track in both languages; lite-05/06 write send time - [Live TS clock validation](/quest/m1/ts-live-clock.md) - resolve the local PCR-rate failure with reproducible timing evidence and CI controls +- [Slow source fails](/quest/m1/slow-source.md) - an export whose source runs further off real time than the output may follow fails promptly naming the rate, instead of dropping frames for hours - [Same-instance TS resume](/quest/m1/ts-same-instance-resume.md) - prove valid continuation and fix the unexpected final exporter failure without weakening identity - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause - [Plan: a public model](/quest/m1/model-public-surface.md) - every crate-private model item is made public or unnecessary, so a custom publisher or subscriber builds from the public model alone diff --git a/quest/m1/slow-source.md b/quest/m1/slow-source.md new file mode 100644 index 00000000000..2f21b2d103f --- /dev/null +++ b/quest/m1/slow-source.md @@ -0,0 +1,47 @@ +# [S] A source the output cannot follow fails the export promptly + +## Goal + +An export whose source arrives further off real time than the output's +clock may follow ends with an error naming the measured rate, once it has +used half its delay. It no longer drops nearly every frame for hours with +only per-frame "missed its deadline" warnings. A short stall, such as a +WebSocket-to-QUIC upgrade, still drops late frames and recovers. + +## Plan + +Found on #5435: before #5292, `moq import` fed a 20 s TS clip about 22% +slow on CI's debug build. The TS export measured -222,000 ppm, dropped +1,273 frames as late, and exited clean with 3.2 s captured. + +`jitter::Buffer` (`rs/moq-mux/src/jitter.rs`) already bails when the source +is out of tolerance, but only once the steered drift sits at `MAX_DRIFT`. +At `MAX_SLEW` (0.075 Hz/s of 27 MHz) the steered clock needs about 3 hours to +reach 30 ppm, so a grossly slow source never trips it. The rate itself is +known within a few `STEER` steps. + +Decided with the maintainer on 2026-10-13: + +- Fail loud, not just warn: a source the output can never follow is refused. +- The check lives in the shared jitter buffer, so TS, FLV, and MKV export + and audio decode fail the same way. +- The rule is the existing one minus "the steered clock is at its limit": + the measured rate is past `MAX_DRIFT` and half the delay is gone. No new + constant. Rejected: a run of a delay's worth of consecutive drops, a new + heuristic beside the rate the buffer already measures. +- Prove with mocked time that a stall shorter than half `RATE_WINDOW` (the + envelope's own bound) doesn't trip it, including an early stall when only + a few floors exist. If the envelope can't tell the two apart that early, + fix the measurement rather than adding a minimum span. +- Regressions: a source 22% slow fails within a few seconds of decode time + with the rate in the error; one 400 ppm off (the existing counted case) + fails once half the delay is gone; sources within 30 ppm never fail. +- Update any doc or comment that says late frames only drop, inline. + +Public API: none; an export that silently degraded now returns an error. +Wire: none. + +## Related + +- [Live TS clock validation](/quest/m1/ts-live-clock.md) - a local run measured -1382 ppm and exited clean; with this rule it fails loudly, which is evidence for that quest, not a blocker +- [#5435](https://github.com/moq-dev/moq/pull/5435) - the duration-fidelity investigation that found this From 7bbe94e204c09368f047652c3fe07087e50f20f2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 13 Oct 2026 12:07:26 -0700 Subject: [PATCH 2/2] quest(m1): scope slow-source to the mux exporters Audio decode uses its own delay estimator in moq-audio, not the shared mux jitter buffer. Co-Authored-By: Claude Opus 5.5 --- quest/m1/slow-source.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quest/m1/slow-source.md b/quest/m1/slow-source.md index 2f21b2d103f..46c8ff1f70a 100644 --- a/quest/m1/slow-source.md +++ b/quest/m1/slow-source.md @@ -24,7 +24,8 @@ Decided with the maintainer on 2026-10-13: - Fail loud, not just warn: a source the output can never follow is refused. - The check lives in the shared jitter buffer, so TS, FLV, and MKV export - and audio decode fail the same way. + fail the same way. Audio decode is out of scope: `moq-audio` sizes its + delay with its own estimator and doesn't steer a clock. - The rule is the existing one minus "the steered clock is at its limit": the measured rate is past `MAX_DRIFT` and half the delay is gone. No new constant. Rejected: a run of a delay's worth of consecutive drops, a new