fix(stats): align rolling directional loss with window counts

This commit is contained in:
Nils Andreas Svee committed 2026-10-04 12:20:47 +07:00
1 parent 524d56f72c
commit 743a050416
8 files changed
+489 -34

No files matched your search

+88 -12
View File
@@ -1,5 +1,6 @@
//! Independent count and time windows must match a filtered replay through
//! StatsCollector, while cumulative accounting retains the complete history.
//! StatsCollector for ordinary metrics, while directional loss has an independent
//! interval model and cumulative accounting retains the complete history.
//! Includes delayed timeout discovery and replies with backdated timestamps.
use std::time::{Duration, Instant, UNIX_EPOCH};
@@ -212,6 +213,90 @@ fn replay(events: &[ClientEvent], late_replies: LateReplyMode) -> Snapshot {
collector.snapshot()
}
/// Retain ordinary metrics through replay, but derive directional loss directly
/// from the full public event history, including the discarded prefix.
fn rolling_reference(
history: &[(ClientEvent, u64)],
retain: impl Fn(usize, u64) -> bool,
late_replies: LateReplyMode,
) -> Snapshot {
let retained: Vec<ClientEvent> = history
.iter()
.enumerate()
.filter(|(index, (_, at))| retain(*index, *at))
.map(|(_, (event, _))| event.clone())
.collect();
let mut snapshot = replay(&retained, late_replies);
let packets: Vec<_> = history
.iter()
.enumerate()
.filter(|(_, (event, _))| {
matches!(
event,
ClientEvent::EchoSent { .. }
| ClientEvent::EchoReply { .. }
| ClientEvent::LateReply { .. }
| ClientEvent::DuplicateReply { .. }
)
})
.collect();
let selected: Vec<_> = packets
.iter()
.enumerate()
.filter(|(_, (index, (_, at)))| retain(*index, *at))
.collect();
let count = |event: &ClientEvent| match event {
ClientEvent::EchoReply { received_stats, .. }
| ClientEvent::LateReply {
sent_at: Some(_),
rtt: Some(_),
received_stats,
..
} => received_stats.and_then(|stats| stats.count).map(u64::from),
_ => None,
};
let delta = selected
.first()
.zip(selected.last())
.and_then(|((first, _), (last, _))| {
// Timestamp filtering must not omit a packet between the endpoints.
if last - first + 1 != selected.len() {
return None;
}
let baseline = if *first == 0 {
Some(0)
} else {
packets[..*first]
.iter()
.filter_map(|(_, (event, _))| count(event))
.max()
}?;
let endpoint = packets[..=*last]
.iter()
.filter_map(|(_, (event, _))| count(event))
.max()?;
Some(endpoint - baseline)
});
snapshot.loss.upstream_loss_packets =
delta.map(|delta| i128::from(snapshot.packets.packets_sent) - i128::from(delta));
snapshot.loss.downstream_loss_packets =
delta.map(|delta| i128::from(delta) - i128::from(snapshot.packets.packets_received));
snapshot.loss.upstream_loss_percent = if snapshot.packets.packets_sent == 0 {
0.0
} else {
snapshot.loss.upstream_loss_packets.map_or(0.0, |loss| {
100.0 * loss as f64 / snapshot.packets.packets_sent as f64
})
};
snapshot.loss.downstream_loss_percent = match delta {
Some(delta) if delta != 0 => {
100.0 * snapshot.loss.downstream_loss_packets.unwrap() as f64 / delta as f64
}
_ => 0.0,
};
snapshot
}
#[test]
fn collector_equality_ignores_expired_time_window_bookkeeping() {
let base = Instant::now();
@@ -342,13 +427,9 @@ proptest! {
// Count window reference: the last `count_limit` retained
// events, ignoring the time bound entirely.
let count_start = history.len().saturating_sub(count_limit);
let count_ref: Vec<ClientEvent> = history[count_start..]
.iter()
.map(|(event, _)| event.clone())
.collect();
prop_assert_eq!(
collector.rolling_count(),
Some(replay(&count_ref, late_replies)),
Some(rolling_reference(&history, |index, _| index >= count_start, late_replies)),
"count window diverged from the count-only reference after op {}", idx
);
@@ -358,14 +439,9 @@ proptest! {
// this anchor backwards.
if let Some(latest_at_ms) = history.iter().map(|(_, at_ms)| at_ms).max() {
let cutoff = latest_at_ms.checked_sub(time_limit_ms);
let time_ref: Vec<ClientEvent> = history
.iter()
.filter(|(_, at_ms)| cutoff.is_none_or(|cutoff| *at_ms >= cutoff))
.map(|(event, _)| event.clone())
.collect();
prop_assert_eq!(
collector.rolling_time(),
Some(replay(&time_ref, late_replies)),
Some(rolling_reference(&history, |_, at_ms| cutoff.is_none_or(|cutoff| at_ms >= cutoff), late_replies)),
"time window diverged from the time-only reference after op {}", idx
);
} else {