From 04d5b634a2421134871d6de2bfcfe407934b5c15 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Wed, 13 May 2026 14:16:07 +0700 Subject: [PATCH] refactor(irtt-stats): extract time stats module --- crates/irtt-stats/src/lib.rs | 144 +--------------------------- crates/irtt-stats/src/time_stats.rs | 143 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 139 deletions(-) create mode 100644 crates/irtt-stats/src/time_stats.rs diff --git a/crates/irtt-stats/src/lib.rs b/crates/irtt-stats/src/lib.rs index d847081..77995c8 100644 --- a/crates/irtt-stats/src/lib.rs +++ b/crates/irtt-stats/src/lib.rs @@ -12,6 +12,11 @@ use irtt_client::{ SignedDuration, }; +mod time_stats; + +use time_stats::TimeMetric; +pub use time_stats::TimeStats; + const CONTINUOUS_SEQUENCE_LIMIT: usize = 4096; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -228,27 +233,6 @@ pub struct ServerProcessingStats { pub processing: TimeStats, } -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct TimeStats { - pub count: u64, - pub total_ns: i128, - pub min_ns: Option, - pub max_ns: Option, - pub mean_ns: f64, - pub median_ns: Option, - pub variance_ns2: f64, -} - -impl TimeStats { - pub fn stddev_ns(&self) -> f64 { - self.variance_ns2.sqrt() - } - - pub fn is_empty(&self) -> bool { - self.count == 0 - } -} - #[derive(Debug, Clone, PartialEq)] struct CoreStats { events: EventCounts, @@ -761,69 +745,6 @@ struct CompletedIpdvPair { receive_ipdv_ns: Option, } -#[derive(Debug, Clone, PartialEq)] -struct TimeMetric { - running: RunningStats, - samples: Option>, -} - -impl TimeMetric { - fn new(retain_samples: bool) -> Self { - Self { - running: RunningStats::default(), - samples: retain_samples.then(Vec::new), - } - } - - fn push_ns(&mut self, value: i128) { - self.running.push(value); - if let Some(samples) = self.samples.as_mut() { - samples.push(value); - } - } - - fn stats(&self) -> TimeStats { - self.running - .stats(self.samples.as_ref().and_then(|samples| median_ns(samples))) - } -} - -#[derive(Debug, Clone, PartialEq, Default)] -struct RunningStats { - count: u64, - total_ns: i128, - min_ns: Option, - max_ns: Option, - mean_ns: f64, - m2_ns2: f64, -} - -impl RunningStats { - fn push(&mut self, value: i128) { - self.count += 1; - self.total_ns = self.total_ns.saturating_add(value); - self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value))); - self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value))); - let x = value as f64; - let delta = x - self.mean_ns; - self.mean_ns += delta / self.count as f64; - let delta2 = x - self.mean_ns; - self.m2_ns2 += delta * delta2; - } - - fn stats(&self, median_ns: Option) -> TimeStats { - TimeStats { - count: self.count, - total_ns: self.total_ns, - min_ns: self.min_ns, - max_ns: self.max_ns, - mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns }, - median_ns, - variance_ns2: sample_variance(self.count, self.m2_ns2), - } - } -} - fn snapshot_window(events: &VecDeque) -> Snapshot { let mut core = CoreStats::new(SampleMode::RunningOnly); for event in events { @@ -832,28 +753,6 @@ fn snapshot_window(events: &VecDeque) -> Snapshot { core.snapshot() } -fn sample_variance(count: u64, m2: f64) -> f64 { - if count < 2 { - 0.0 - } else { - m2 / (count - 1) as f64 - } -} - -fn median_ns(samples: &[i128]) -> Option { - if samples.is_empty() { - return None; - } - let mut sorted = samples.to_vec(); - sorted.sort_unstable(); - let mid = sorted.len() / 2; - Some(if sorted.len() % 2 == 1 { - sorted[mid] as f64 - } else { - (sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0 - }) -} - fn loss_stats(packets: PacketCounts) -> LossStats { let lost = packets.packets_sent.saturating_sub(packets.unique_replies); let packet_loss_percent = if packets.packets_sent == 0 { @@ -1016,39 +915,6 @@ mod tests { } } - #[test] - fn running_duration_stats_use_sample_variance() { - let mut metric = TimeMetric::new(false); - metric.push_ns(1); - metric.push_ns(2); - metric.push_ns(3); - let stats = metric.stats(); - assert_eq!(stats.count, 3); - assert_eq!(stats.total_ns, 6); - assert_eq!(stats.min_ns, Some(1)); - assert_eq!(stats.max_ns, Some(3)); - assert_eq!(stats.mean_ns, 2.0); - assert_eq!(stats.variance_ns2, 1.0); - assert_eq!(stats.stddev_ns(), 1.0); - } - - #[test] - fn exact_median_handles_odd_and_even_samples() { - assert_eq!(median_ns(&[3, 1, 2]), Some(2.0)); - assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5)); - assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0)); - assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0)); - } - - #[test] - fn single_sample_stddev_is_zero() { - let mut metric = TimeMetric::new(false); - metric.push_ns(42); - let stats = metric.stats(); - assert_eq!(stats.variance_ns2, 0.0); - assert_eq!(stats.stddev_ns(), 0.0); - } - #[test] fn system_time_before_epoch_is_supported() { let before = UNIX_EPOCH - Duration::from_nanos(7); diff --git a/crates/irtt-stats/src/time_stats.rs b/crates/irtt-stats/src/time_stats.rs new file mode 100644 index 0000000..beda0e3 --- /dev/null +++ b/crates/irtt-stats/src/time_stats.rs @@ -0,0 +1,143 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TimeStats { + pub count: u64, + pub total_ns: i128, + pub min_ns: Option, + pub max_ns: Option, + pub mean_ns: f64, + pub median_ns: Option, + pub variance_ns2: f64, +} + +impl TimeStats { + pub fn stddev_ns(&self) -> f64 { + self.variance_ns2.sqrt() + } + + pub fn is_empty(&self) -> bool { + self.count == 0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct TimeMetric { + running: RunningStats, + samples: Option>, +} + +impl TimeMetric { + pub(crate) fn new(retain_samples: bool) -> Self { + Self { + running: RunningStats::default(), + samples: retain_samples.then(Vec::new), + } + } + + pub(crate) fn push_ns(&mut self, value: i128) { + self.running.push(value); + if let Some(samples) = self.samples.as_mut() { + samples.push(value); + } + } + + pub(crate) fn stats(&self) -> TimeStats { + self.running + .stats(self.samples.as_ref().and_then(|samples| median_ns(samples))) + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +struct RunningStats { + count: u64, + total_ns: i128, + min_ns: Option, + max_ns: Option, + mean_ns: f64, + m2_ns2: f64, +} + +impl RunningStats { + fn push(&mut self, value: i128) { + self.count += 1; + self.total_ns = self.total_ns.saturating_add(value); + self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value))); + self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value))); + let x = value as f64; + let delta = x - self.mean_ns; + self.mean_ns += delta / self.count as f64; + let delta2 = x - self.mean_ns; + self.m2_ns2 += delta * delta2; + } + + fn stats(&self, median_ns: Option) -> TimeStats { + TimeStats { + count: self.count, + total_ns: self.total_ns, + min_ns: self.min_ns, + max_ns: self.max_ns, + mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns }, + median_ns, + variance_ns2: sample_variance(self.count, self.m2_ns2), + } + } +} + +fn sample_variance(count: u64, m2: f64) -> f64 { + if count < 2 { + 0.0 + } else { + m2 / (count - 1) as f64 + } +} + +fn median_ns(samples: &[i128]) -> Option { + if samples.is_empty() { + return None; + } + let mut sorted = samples.to_vec(); + sorted.sort_unstable(); + let mid = sorted.len() / 2; + Some(if sorted.len() % 2 == 1 { + sorted[mid] as f64 + } else { + (sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0 + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn running_duration_stats_use_sample_variance() { + let mut metric = TimeMetric::new(false); + metric.push_ns(1); + metric.push_ns(2); + metric.push_ns(3); + let stats = metric.stats(); + assert_eq!(stats.count, 3); + assert_eq!(stats.total_ns, 6); + assert_eq!(stats.min_ns, Some(1)); + assert_eq!(stats.max_ns, Some(3)); + assert_eq!(stats.mean_ns, 2.0); + assert_eq!(stats.variance_ns2, 1.0); + assert_eq!(stats.stddev_ns(), 1.0); + } + + #[test] + fn exact_median_handles_odd_and_even_samples() { + assert_eq!(median_ns(&[3, 1, 2]), Some(2.0)); + assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5)); + assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0)); + assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0)); + } + + #[test] + fn single_sample_stddev_is_zero() { + let mut metric = TimeMetric::new(false); + metric.push_ns(42); + let stats = metric.stats(); + assert_eq!(stats.variance_ns2, 0.0); + assert_eq!(stats.stddev_ns(), 0.0); + } +}