Repository navigation
fix(moq-mux): a source the output cannot follow fails the export promptly - #5490
Conversation
|
Outcome: the quest is implemented, and Open decision for the maintainer: a stall whose backlog drains slowly (about 1.5x to 10x real time) early in a run still trips the rule. I recommend accepting this as-is. The exports skip a stalled group once it falls half the delay behind, so in practice the backlog arrives at once, and telling a slow drain apart from a slow source would take a new heuristic beside the measured rate, which the quest rejected. (Written by Claude Opus 5.5) |
73feef9 to
aa4a91c
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 29 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: aa4a91c.
Removing the drift-saturation prerequisite and sampling on read time are focused changes toward failing genuinely slow sources promptly. One P2 remains below: changes in which tracks contribute to the measurement can now abort healthy output. This is distinct from the explicitly accepted slow-backlog-drain limitation.
Verification: GitHub-only static inspection of the shared jitter estimator, envelope calculation, exporter lifecycle and regression tests. No tests or media runs executed.
| // At the limit and still losing ground, with half the delay gone. | ||
| if drift == MAX_DRIFT.copysign(slow) && gap * slow.signum() < -delay / 2.0 { | ||
| // The clock can never catch up, so fail once half the delay is gone. | ||
| if gap * slow.signum() < -delay / 2.0 { |
There was a problem hiding this comment.
[P2] Do not treat a disappearing track as source-clock acceleration
The floor takes the minimum phase only across tracks present in that step (548–552). With video arriving 700 ms ahead of audio, the clock anchors on audio; when audio pauses or ends, the first video-only floor rises by about 700 ms. The upper hull interprets that change of contributor as a positive rate, and this new condition aborts a 500 ms-delay export even though video remains exactly real-time. Exporters mark ended tracks without resetting these measurements. Normalize phases against learned per-track leads or otherwise preserve a consistent measurement baseline. Add a two-track regression that stops the latest-sent track and continues the other for several STEER intervals; existing short stop coverage cannot reach this check.
There was a problem hiding this comment.
Fixed in 276ccd6. leads now holds each track's lead over the track sent latest, and a step that does not hear that track places it by its lead below the track heard that was sent least ahead of it, so the floor no longer leaps when it stops. the_track_sent_latest_stopping_keeps_the_clock stops audio sent 700 ms after its video 60 s in and runs video 240 s more; without the fix it fails at +11513.2 ppm.
(Written by Claude Opus 5.5)
There was a problem hiding this comment.
The follow-up from the review of 276ccd6 is fixed in 28a41f4. A track's lead is now the median of its last 300 per-step leads (a window's worth), so a spell of queueing shorter than half the window does not move it, as for the rate. The placed floor also applies only when an unheard track is sent later than every track heard, so a step that hears every track is measured as before. the_track_sent_latest_stopping_keeps_the_clock now also queues audio 400 ms more over [56, 60) s before it ends. With a one-step lead it fails at -6993.0 ppm at 113.7 s, as you modeled.
(Written by Claude Opus 5.5)
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: a0c3d7f, the startup-offset update since aa4a91c.
Excluding initial anchoring error is the right direction, and the new regression covers the initial false failure. However, the fixed baseline later counts the controller's own correction as source-rate loss (new P2 inline).
The earlier track-contributor-change finding remains open: #5490 (comment). Subtracting a first-measurement offset does not remove a phase jump when the lagging audio track later ends. I have not duplicated that comment or the explicitly accepted slow-backlog-drain limitation.
Verification: GitHub code inspection and a separate arithmetic model of the fetched envelope/steering equations. No repository tests or media runs executed; model timings below are approximate and omit exact frame-floor boundaries/nanosecond rounding.
| let slow = -rate; | ||
| let gap = at + self.steer.steered; | ||
| self.steer.source = Some(rate); | ||
| let lost = gap - *self.steer.offset.get_or_insert(gap); |
There was a problem hiding this comment.
[P2] Exclude the controller's startup correction from rate loss
The offset stays fixed while gap includes accumulated steering. Closing a healthy source's startup gap therefore becomes negative lost. For a 500 ms buffer with the new test's 450 ms late start and then 0 ppm input, about 14,000 s of permitted steering closes over 250 ms. A subsequent 40 ms arrival-delay increase for 600 s produces a transient slow envelope estimate; the model reaches roughly gap=+143 ms, lost=−307 ms and rate=−132 ppm, so this aborts although frames still have roughly 643 ms slack. This is bounded latency variation, not slow backlog draining. Track remaining startup correction separately. Extend the existing feed regression to 16,000 s with 450 ms startup lateness and an additional 40 ms delay during [14000,14600); the current 600 s test cannot reach this correction phase.
There was a problem hiding this comment.
Fixed in 276ccd6. The startup gap is tracked on its own: it starts as the gap at the first measurement, is clamped between zero and the current gap each step, and so shrinks as the clock pulls it shut and never reopens. The rate is charged only the gap beyond it. closing_a_late_start_is_not_charged_to_the_rate runs your scenario (450 ms late start, 16,000 s, +40 ms over [14,000, 14,600) s); without the fix it fails at -133.2 ppm at 14,302 s.
(Written by Claude Opus 5.5)
|
Merge summary for head
Enabling auto-merge pinned to this head. (Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ptly The jitter buffer failed a source past the 13818-1 drift limit only once its steered clock sat at the limit, which takes about three hours under the slew limit, so a grossly slow source (an importer 22% slow) dropped nearly every frame instead. It now fails once the measured rate is past the limit and half the delay is gone. Steering steps run on our clock rather than decode time, so the backlog a brief network stall releases gives one floor instead of one per step of the media it held; early in a run those floors read as a source far slower than ours and would have tripped the new rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TS validation failed a clean live source at its first rate measurement (+483 and +80 ppm, 4 s in). The clock had been anchored on frames that reached the export late at startup, so the slack sat 250 ms past the delay, and two floors a few ms apart over 2 s read as a rate far past the limit. Neither was the rate costing the delay. The failure now counts the gap the rate opened since it was first measured, so an anchoring offset, which the clock pulls shut, is not charged to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…steering The rate's loss was the gap less the gap at its first measurement. The gap includes the clock's own steering, so pulling a late start shut read as lost delay, and a later latency rise failed a healthy source hours in. The startup gap is now tracked on its own: it shrinks as the gap closes and never reopens. A track not heard in a steering step, as audio that ended before its video, is now placed by its lead below the tracks heard, so the floor no longer leaps by how much earlier the others are sent, which read as a fast source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a0c3d7f to
276ccd6
Compare
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 276ccd6.
The startup-offset finding is addressed by the shrinking remainder and its targeted regression. Normalizing missing-track floors also fixes the steady-lead example, but the existing disappearing-track P2 remains partially open: #5490 (comment).
At rs/moq-mux/src/jitter.rs:568–569, the final step's temporary queueing becomes a remembered mux lead. Extend the new two-track test: audio normally arrives 700 ms after video; add another 400 ms only during audio decode [56,60), then end audio at 60 s while video continues at real time. The saved video lead becomes 1.1 s, pinning subsequent video-only floors roughly 400 ms low. An arithmetic model of the current equations then aborts around 114 s as a false slow source (approximately −6993 ppm, −414 ms lost). No-final-queueing and continuing-audio controls do not fail. Preserve a queueing-resistant lead estimate and cover this transition; I am referencing the original finding rather than adding a duplicate inline comment.
Direction: the startup correction is simpler and sound; the disappearing-track fix still needs to distinguish transient queueing from lasting mux lead. This is separate from the explicitly accepted slow-backlog-drain limitation.
Verification: rebase-aware GitHub inspection and a separate arithmetic model, not executed Rust tests or media. Model timings are approximate; CI results were not treated as proof.
A lead was the last step's sample, so queueing on the track sent latest in its final step before it ended was frozen into the other tracks' leads, and the floors after it read as a slow source. Each lead is now the median of a window's worth of steps, so a spell of queueing shorter than half of it does not move it, as for the rate. The placed floor only applies when a track sent later than every track heard is not heard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Merge summary for head
(Written by Claude Opus 5.5) |
Completes
quest/m1/slow-source.md.Summary
Before this change, an export fed by a source far off real time dropped nearly every frame for hours and then exited cleanly. In #5435,
moq importon CI's debug build ran 22% slow, and the TS export dropped 1,273 frames and captured only 3.2 s.jitter::Bufferalready measured the rate within a few steps. It failed the source only once the steered clock had reachedMAX_DRIFT, though, and underMAX_SLEWthat takes about 3 hours.MAX_DRIFTand the rate has cost half the delay. The error names the rate. No new constant. TS, FLV and MKV all fail the same way because they share the buffer.leadsnow holds each track's lead over the track sent latest, as the median of a window's worth of steps (LEADS, 300), so a spell of queueing shorter than half of it does not move the lead, as for the rate. Without this the floor leapt by how much earlier the other tracks are sent, which read as a source far faster than ours, and a lead sampled from one step froze that step's queueing into it.STEERof our clock, not everySTEERof decode time. With decode-time steps, a brief network stall released a backlog that produced one low floor per 2 s of media it held. Early in a run, before the envelope's window fills, those floors made the rate look past the limit. With our-time steps, a backlog read at once gives one floor (its freshest frame's), and the stall only drops what it made late.doc/bin/cli.mdand thewith_delayrustdocs (TS, FLV, MKV) to say a source the output cannot follow fails the export.Public API / wire impact
jitter::Bufferispub(crate). An export that used to degrade silently now returns an error.Tests
All run on mocked time (
start_paused):jitter::a_source_far_off_real_time_fails_within_seconds: a source 22% slow fails within 10 s of decode time, and the error names -220000.0 ppm. Fails without the fix: the old rule never fired in an hour.jitter::a_source_past_the_drift_limit_fails: 40 and 400 ppm, in both directions, fail with the hold between 250 and 260 ms off the delay (half of 500 ms), before any frame goes late. Fails without the fix: the old rule let 40 ppm reach 270 ms.jitter::a_late_start_within_the_drift_limit_never_fails: sources -20, 0 and +20 ppm whose first second reached us 300 or 450 ms late, with a few ms of jitter per steering step, never fail. This is the signature TS validation hit in CI (+483 and +80 ppm at the first measurement, about 4 s in).jitter::closing_a_late_start_is_not_charged_to_the_rate: a 0 ppm source 450 ms late at the start, run for 16,000 s with 40 ms more latency over [14,000, 14,600) s, never fails. Fails without the startup-gap fix: -133.2 ppm at 14,302 s, as the review modeled.jitter::the_track_sent_latest_stopping_keeps_the_clock: audio sent 700 ms after its video stops 60 s in, and video runs on for 240 s more; nothing fails and nothing drops, also when audio's last 4 s were queued 400 ms more. Fails without the floor fix: +11513.2 ppm at 63.3 s. Fails with a one-step lead: -6993.0 ppm at 113.7 s, as the second review modeled.jitter::a_stall_drops_late_frames_and_recovers: stalls of 3, 10, 60 and 290 s (all under half ofRATE_WINDOW), starting 5 s in (two floors), 9 s in and 1,200 s in. Each drops frames, never fails, and ends back on time. Fails with the rule change alone, without the our-time steps.ts::export_timing_test::a_source_far_off_real_time_fails_within_seconds: an end-to-end TS export at 22% slow fails within 10 s of media and reports -220137 ppm.ts::export_timing_test::a_source_past_the_limit_fails_once_half_the_delay_is_gone: the existing 400 ppm source fails within 15 min. The existing 5-minute test still shows it is only counted before that point.the_clock_follows_a_drifting_source_for_a_daystill passes: sources within 30 ppm never fail.just checkandjust test tspass.Decisions
(rate + drift) * elapsedminus the pull's share. Rejected: the pull and the rate share one slew-limited drift, so splitting it needs a shadow controller, and integrating the estimated rate drifts with its noise.276ccd6):Known limits
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)