From 71b5819366f8d8bb84a2859ac1d6117e3be44109 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Sat, 15 Aug 2026 10:48:45 +0700 Subject: [PATCH] docs(stats): scope the exact-median contract to the cumulative snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rolling snapshots are recomputed with `SampleMode::RunningOnly` regardless of the configured sample mode, so the new `SampleMode::Exact` wording — that every timing metric a `Snapshot` exposes reports a median — was not true of `rolling_count()` and `rolling_time()`. Scope the promise to the cumulative snapshot and say plainly on both rolling accessors that they use running statistics only and report no median, since a rolling window is a bounded recent view rather than the run's retained history. A test pins both halves so the wording cannot quietly go stale. Rolling retention behavior itself is unchanged. --- crates/irtt-stats/src/lib.rs | 32 ++++++++++++++++++-------- crates/irtt-stats/tests/stats.rs | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/crates/irtt-stats/src/lib.rs b/crates/irtt-stats/src/lib.rs index a3a31a3..dbde3aa 100644 --- a/crates/irtt-stats/src/lib.rs +++ b/crates/irtt-stats/src/lib.rs @@ -28,7 +28,8 @@ pub use time_stats::TimeStats; #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Configuration for statistics collection. pub struct StatsConfig { - /// How timing samples are retained, and so whether medians are available. + /// How timing samples are retained, and so whether the cumulative snapshot + /// reports medians. pub samples: SampleMode, /// Number of recent normalized events retained for count-based rolling snapshots. /// @@ -45,9 +46,9 @@ impl StatsConfig { /// Returns the default configuration for finite runs. /// /// Finite mode retains exact samples for every timing metric, so each of - /// them reports a median once it has samples, and keeps unbounded - /// adjacent-sequence IPDV tracking so late adjacent replies can still - /// complete IPDV pairs. + /// them reports a median in the cumulative snapshot once it has samples, + /// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent + /// replies can still complete IPDV pairs. pub fn finite() -> Self { Self { samples: SampleMode::Exact, @@ -154,15 +155,18 @@ impl Default for StatsConfig { #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Controls whether exact timing samples are retained. /// -/// The choice applies uniformly to every timing metric a [`Snapshot`] exposes, -/// so a snapshot never mixes median-capable and median-incapable timing -/// metrics. +/// This governs the cumulative snapshot from [`StatsCollector::snapshot`], +/// where the choice applies uniformly to every timing metric, so that snapshot +/// never mixes median-capable and median-incapable timing metrics. Rolling +/// snapshots are recomputed with running statistics whatever this is set to; +/// see [`StatsCollector::rolling_count`]. pub enum SampleMode { /// Keep only running statistics; medians are not available for any timing /// metric, and retention stays bounded for long-running sessions. RunningOnly, /// Retain exact samples for every timing metric, so each one reports a - /// median once it has samples. Retention grows with the probe count. + /// median once it has samples in the cumulative snapshot. Retention grows + /// with the probe count. Exact, } @@ -201,20 +205,28 @@ impl StatsCollector { } /// Returns a snapshot of all events processed by this collector. + /// + /// This is the snapshot [`StatsConfig::samples`] applies to, so under + /// [`SampleMode::Exact`] every timing metric here reports a median once it + /// has samples. pub fn snapshot(&self) -> Snapshot { self.cumulative.snapshot() } /// Returns the configured count-based rolling snapshot, if enabled. /// - /// The snapshot is recomputed from the retained rolling events. + /// The snapshot is recomputed from the retained rolling events using + /// running statistics only, whatever [`StatsConfig::samples`] is set to, + /// so its timing metrics report no median. A rolling window is a bounded + /// recent view rather than the run's retained history. pub fn rolling_count(&self) -> Option { self.rolling.count_snapshot() } /// Returns the configured time-based rolling snapshot, if enabled. /// - /// The snapshot is recomputed from the retained rolling events. + /// Like [`StatsCollector::rolling_count`], this is recomputed with running + /// statistics only and reports no medians. pub fn rolling_time(&self) -> Option { self.rolling.time_snapshot() } diff --git a/crates/irtt-stats/tests/stats.rs b/crates/irtt-stats/tests/stats.rs index 002ec1d..616c51b 100644 --- a/crates/irtt-stats/tests/stats.rs +++ b/crates/irtt-stats/tests/stats.rs @@ -664,6 +664,45 @@ fn untracked_late_replies_stay_untracked_in_both_policies() { } } +#[test] +fn rolling_snapshots_report_no_medians_even_in_exact_mode() { + let mut collector = StatsCollector::new(StatsConfig { + rolling_count: Some(8), + rolling_time: Some(Duration::from_secs(60)), + ..StatsConfig::finite() + }); + for seq in 0..3 { + collector.process(&sent(seq, ts(u64::from(seq) * 10))); + collector.process(&adjusted_reply( + seq, + 10 + u64::from(seq), + 9 + i128::from(seq), + )); + } + + // The cumulative snapshot keeps the exact-mode contract... + let cumulative = collector.snapshot(); + assert!(cumulative.rtt.primary.median_ns.is_some()); + assert!(cumulative.send_call.median_ns.is_some()); + assert!(cumulative.server_processing.processing.median_ns.is_some()); + + // ...while a rolling window is a bounded recent view recomputed from + // running statistics, so it reports no median whatever the sample mode. + for rolling in [ + collector.rolling_count().unwrap(), + collector.rolling_time().unwrap(), + ] { + assert!( + rolling.rtt.primary.count > 0, + "the window should have samples" + ); + assert_eq!(rolling.rtt.primary.median_ns, None); + assert_eq!(rolling.send_call.median_ns, None); + assert_eq!(rolling.timer_error.median_ns, None); + assert_eq!(rolling.server_processing.processing.median_ns, None); + } +} + #[test] fn rolling_snapshots_use_the_same_late_reply_policy() { let mut collector = StatsCollector::new(StatsConfig {