Skip to main content

irtt_stats/
time_stats.rs

1#[derive(Debug, Clone, Copy, PartialEq)]
2/// Summary statistics for timing values.
3///
4/// All fields use nanoseconds except `variance_ns2`, which is nanoseconds
5/// squared. Values may represent signed timing quantities such as scheduling
6/// error or one-way delay.
7pub struct TimeStats {
8    /// Number of samples included in this summary.
9    pub count: u64,
10    /// Sum of all samples, in nanoseconds.
11    pub total_ns: i128,
12    /// Smallest sample, in nanoseconds.
13    pub min_ns: Option<i128>,
14    /// Largest sample, in nanoseconds.
15    pub max_ns: Option<i128>,
16    /// Arithmetic mean, in nanoseconds.
17    pub mean_ns: f64,
18    /// Median, in nanoseconds, when exact samples were retained.
19    pub median_ns: Option<f64>,
20    /// Sample variance, in nanoseconds squared.
21    pub variance_ns2: f64,
22}
23
24impl TimeStats {
25    /// Returns the sample standard deviation, in nanoseconds.
26    pub fn stddev_ns(&self) -> f64 {
27        self.variance_ns2.sqrt()
28    }
29
30    /// Returns whether this summary contains no samples.
31    pub fn is_empty(&self) -> bool {
32        self.count == 0
33    }
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub(crate) struct TimeMetric {
38    running: RunningStats,
39    samples: Option<Vec<i128>>,
40}
41
42impl TimeMetric {
43    pub(crate) fn new(retain_samples: bool) -> Self {
44        Self {
45            running: RunningStats::default(),
46            samples: retain_samples.then(Vec::new),
47        }
48    }
49
50    pub(crate) fn push_ns(&mut self, value: i128) {
51        self.running.push(value);
52        if let Some(samples) = self.samples.as_mut() {
53            samples.push(value);
54        }
55    }
56
57    pub(crate) fn stats(&self) -> TimeStats {
58        self.running
59            .stats(self.samples.as_ref().and_then(|samples| median_ns(samples)))
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Default)]
64struct RunningStats {
65    count: u64,
66    total_ns: i128,
67    min_ns: Option<i128>,
68    max_ns: Option<i128>,
69    mean_ns: f64,
70    m2_ns2: f64,
71}
72
73impl RunningStats {
74    fn push(&mut self, value: i128) {
75        self.count += 1;
76        self.total_ns = self.total_ns.saturating_add(value);
77        self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value)));
78        self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value)));
79        let x = value as f64;
80        let delta = x - self.mean_ns;
81        self.mean_ns += delta / self.count as f64;
82        let delta2 = x - self.mean_ns;
83        self.m2_ns2 += delta * delta2;
84    }
85
86    fn stats(&self, median_ns: Option<f64>) -> TimeStats {
87        TimeStats {
88            count: self.count,
89            total_ns: self.total_ns,
90            min_ns: self.min_ns,
91            max_ns: self.max_ns,
92            mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns },
93            median_ns,
94            variance_ns2: sample_variance(self.count, self.m2_ns2),
95        }
96    }
97}
98
99fn sample_variance(count: u64, m2: f64) -> f64 {
100    if count < 2 {
101        0.0
102    } else {
103        m2 / (count - 1) as f64
104    }
105}
106
107fn median_ns(samples: &[i128]) -> Option<f64> {
108    if samples.is_empty() {
109        return None;
110    }
111    let mut sorted = samples.to_vec();
112    sorted.sort_unstable();
113    let mid = sorted.len() / 2;
114    Some(if sorted.len() % 2 == 1 {
115        sorted[mid] as f64
116    } else {
117        (sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
118    })
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn running_duration_stats_use_sample_variance() {
127        let mut metric = TimeMetric::new(false);
128        metric.push_ns(1);
129        metric.push_ns(2);
130        metric.push_ns(3);
131        let stats = metric.stats();
132        assert_eq!(stats.count, 3);
133        assert_eq!(stats.total_ns, 6);
134        assert_eq!(stats.min_ns, Some(1));
135        assert_eq!(stats.max_ns, Some(3));
136        assert_eq!(stats.mean_ns, 2.0);
137        assert_eq!(stats.variance_ns2, 1.0);
138        assert_eq!(stats.stddev_ns(), 1.0);
139    }
140
141    #[test]
142    fn exact_median_handles_odd_and_even_samples() {
143        assert_eq!(median_ns(&[3, 1, 2]), Some(2.0));
144        assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5));
145        assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0));
146        assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0));
147    }
148
149    #[test]
150    fn single_sample_stddev_is_zero() {
151        let mut metric = TimeMetric::new(false);
152        metric.push_ns(42);
153        let stats = metric.stats();
154        assert_eq!(stats.variance_ns2, 0.0);
155        assert_eq!(stats.stddev_ns(), 0.0);
156    }
157}