Repository navigation
fix(mux): aggregate TS damage warnings per PID burst #5263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8f90639
chore(quest): claim TS damage log aggregation
kixelated 776de77
fix(mux): aggregate TS damage warnings by PID burst
kixelated 0615d10
Merge origin/main for TS damage-log iteration
kixelated 8084843
fix(mux): recognize quiet PID bursts between summary ticks
kixelated bde36c7
fix(mux): summarize the final TS damage burst on every terminal path
kixelated File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| //! Bounded warning bursts for damaged transport units. | ||
|
|
||
| use std::collections::BTreeMap; | ||
|
|
||
| use web_async::time::Instant; | ||
|
|
||
| use super::stats; | ||
|
|
||
| #[derive(Default)] | ||
| pub(super) struct Log { | ||
| bursts: BTreeMap<u16, Burst>, | ||
| next: Option<Instant>, | ||
| } | ||
|
|
||
| struct Burst { | ||
| last: Instant, | ||
| pending: u64, | ||
| } | ||
|
|
||
| impl Burst { | ||
| fn flush(&mut self, pid: u16) { | ||
| let count = std::mem::take(&mut self.pending); | ||
| if count != 0 { | ||
| tracing::warn!(pid, count, "TS damaged-unit summary"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Log { | ||
| pub fn record(&mut self, pid: u16, error: &anyhow::Error) { | ||
| let now = Instant::now(); | ||
| self.flush(now); | ||
| let first = match self.bursts.entry(pid) { | ||
| std::collections::btree_map::Entry::Occupied(mut entry) => { | ||
| let burst = entry.get_mut(); | ||
| // A PID's quiet interval can end before the global summary cadence. | ||
| let first = now.duration_since(burst.last) >= stats::Log::INTERVAL; | ||
| if first { | ||
| burst.flush(pid); | ||
| } else { | ||
| burst.pending += 1; | ||
| } | ||
| burst.last = now; | ||
| first | ||
| } | ||
| std::collections::btree_map::Entry::Vacant(entry) => { | ||
| entry.insert(Burst { last: now, pending: 0 }); | ||
| true | ||
| } | ||
| }; | ||
| if first { | ||
| tracing::warn!(pid, count = 1, %error, "dropped a damaged TS unit"); | ||
| } | ||
| self.next.get_or_insert(now + stats::Log::INTERVAL); | ||
| } | ||
|
|
||
| pub fn flush(&mut self, now: Instant) { | ||
| if self.next.is_none_or(|next| now < next) { | ||
| return; | ||
| } | ||
| self.bursts.retain(|&pid, burst| { | ||
| burst.flush(pid); | ||
| // A quiet interval ends the burst, so its next damage warns immediately. | ||
| now.duration_since(burst.last) < stats::Log::INTERVAL | ||
| }); | ||
| self.next = (!self.bursts.is_empty()).then_some(now + stats::Log::INTERVAL); | ||
| } | ||
| } | ||
|
|
||
| impl Drop for Log { | ||
| // Input can end through finish, abort, or an error, so the final burst summarizes on drop. | ||
| fn drop(&mut self) { | ||
| for (&pid, burst) in &mut self.bursts { | ||
| burst.flush(pid); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[tokio::test(start_paused = true)] | ||
| #[ignore = "benchmark; runs in nightly"] | ||
| async fn benchmark_damage_pid_fanout() { | ||
| let error = anyhow::anyhow!("broken PES"); | ||
| eprintln!("pids,damaged_units,ns_per_unit"); | ||
| for pids in [1_u16, 64, 8192] { | ||
| for units in [1, 16, 128] { | ||
| let mut log = Log::default(); | ||
| for pid in 0..pids { | ||
| log.record(pid, &error); | ||
| } | ||
| for _ in 0..100 { | ||
| log.record(pids - 1, &error); | ||
| } | ||
| // Media time stays paused, so this measures one touched PID's hot path, | ||
| // independently of periodic summaries that visit every active burst. | ||
| let start = std::time::Instant::now(); | ||
| for _ in 0..10_000 { | ||
| for _ in 0..units { | ||
| log.record(std::hint::black_box(pids - 1), &error); | ||
| } | ||
| } | ||
| let nanos = start.elapsed().as_nanos() as f64 / (10_000 * units) as f64; | ||
| eprintln!("{pids},{units},{nanos:.1}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[tokio::test(start_paused = true)] | ||
| #[tracing_test::traced_test] | ||
| async fn a_quiet_pid_starts_a_new_burst_between_summary_ticks() { | ||
| let mut log = Log::default(); | ||
| let error = anyhow::anyhow!("broken PES"); | ||
| log.record(97, &error); | ||
| tokio::time::advance(stats::Log::INTERVAL / 2).await; | ||
| log.record(97, &error); | ||
| tokio::time::advance(stats::Log::INTERVAL / 2).await; | ||
| log.flush(Instant::now()); | ||
| // This PID is quiet for a full interval before the next global summary tick. | ||
| tokio::time::advance(stats::Log::INTERVAL / 2).await; | ||
| log.record(97, &error); | ||
| logs_assert(|lines| { | ||
| let first = lines | ||
| .iter() | ||
| .filter(|line| line.contains("dropped a damaged TS unit")) | ||
| .count(); | ||
| if first == 2 { | ||
| Ok(()) | ||
| } else { | ||
| Err(format!("expected an immediate new-burst warning, got {first}")) | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| #[tokio::test(start_paused = true)] | ||
| #[tracing_test::traced_test] | ||
| async fn a_continuous_burst_logs_one_summary_per_interval() { | ||
| let mut log = Log::default(); | ||
| let error = anyhow::anyhow!("broken PES"); | ||
| log.record(97, &error); | ||
| for _ in 0..4 { | ||
| tokio::time::advance(stats::Log::INTERVAL / 2).await; | ||
| log.record(97, &error); | ||
| } | ||
| drop(log); | ||
| logs_assert(|lines| { | ||
| let first = lines | ||
| .iter() | ||
| .filter(|line| line.contains("dropped a damaged TS unit")) | ||
| .count(); | ||
| let summaries: Vec<_> = lines | ||
| .iter() | ||
| .filter(|line| line.contains("TS damaged-unit summary")) | ||
| .collect(); | ||
| if first == 1 && summaries.len() == 3 && summaries[1].contains("count=2") { | ||
| Ok(()) | ||
| } else { | ||
| Err(format!( | ||
| "expected one burst and three summaries, got {first}/{summaries:?}" | ||
| )) | ||
| } | ||
| }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a caller successfully invokes
Import::finish()but retains the importer, the final aggregate remains pending indefinitely because thisDropimplementation is now the only unconditional terminal flush. This is supported by the publicfinish(&mut self)API and wrappers such as the SRT publisher, and the new regression test's explicitdrop(import)afterfinish()confirms thatfinish()itself does not emit the summary. Keep the drop fallback for abort and error paths, but also flush pending summaries before a successfulfinish()returns.Useful? React with 👍 / 👎.