docs(stats): scope the exact-median contract to the cumulative snapshot

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.
This commit is contained in:
Nils Andreas Svee committed 2026-08-15 10:48:45 +07:00
1 parent db9bd515bb
commit 71b5819366
2 files changed
+61 -10

No files matched your search

+22 -10
View File
@@ -28,7 +28,8 @@ pub use time_stats::TimeStats;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Configuration for statistics collection. /// Configuration for statistics collection.
pub struct StatsConfig { 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, pub samples: SampleMode,
/// Number of recent normalized events retained for count-based rolling snapshots. /// Number of recent normalized events retained for count-based rolling snapshots.
/// ///
@@ -45,9 +46,9 @@ impl StatsConfig {
/// Returns the default configuration for finite runs. /// Returns the default configuration for finite runs.
/// ///
/// Finite mode retains exact samples for every timing metric, so each of /// Finite mode retains exact samples for every timing metric, so each of
/// them reports a median once it has samples, and keeps unbounded /// them reports a median in the cumulative snapshot once it has samples,
/// adjacent-sequence IPDV tracking so late adjacent replies can still /// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent
/// complete IPDV pairs. /// replies can still complete IPDV pairs.
pub fn finite() -> Self { pub fn finite() -> Self {
Self { Self {
samples: SampleMode::Exact, samples: SampleMode::Exact,
@@ -154,15 +155,18 @@ impl Default for StatsConfig {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Controls whether exact timing samples are retained. /// Controls whether exact timing samples are retained.
/// ///
/// The choice applies uniformly to every timing metric a [`Snapshot`] exposes, /// This governs the cumulative snapshot from [`StatsCollector::snapshot`],
/// so a snapshot never mixes median-capable and median-incapable timing /// where the choice applies uniformly to every timing metric, so that snapshot
/// metrics. /// 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 { pub enum SampleMode {
/// Keep only running statistics; medians are not available for any timing /// Keep only running statistics; medians are not available for any timing
/// metric, and retention stays bounded for long-running sessions. /// metric, and retention stays bounded for long-running sessions.
RunningOnly, RunningOnly,
/// Retain exact samples for every timing metric, so each one reports a /// 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, Exact,
} }
@@ -201,20 +205,28 @@ impl StatsCollector {
} }
/// Returns a snapshot of all events processed by this collector. /// 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 { pub fn snapshot(&self) -> Snapshot {
self.cumulative.snapshot() self.cumulative.snapshot()
} }
/// Returns the configured count-based rolling snapshot, if enabled. /// 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<Snapshot> { pub fn rolling_count(&self) -> Option<Snapshot> {
self.rolling.count_snapshot() self.rolling.count_snapshot()
} }
/// Returns the configured time-based rolling snapshot, if enabled. /// 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<Snapshot> { pub fn rolling_time(&self) -> Option<Snapshot> {
self.rolling.time_snapshot() self.rolling.time_snapshot()
} }
+39
View File
@@ -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] #[test]
fn rolling_snapshots_use_the_same_late_reply_policy() { fn rolling_snapshots_use_the_same_late_reply_policy() {
let mut collector = StatsCollector::new(StatsConfig { let mut collector = StatsCollector::new(StatsConfig {