1#[derive(Debug, Clone, Copy, PartialEq)]
2pub struct TimeStats {
8 pub count: u64,
10 pub total_ns: i128,
12 pub min_ns: Option<i128>,
14 pub max_ns: Option<i128>,
16 pub mean_ns: f64,
18 pub median_ns: Option<f64>,
20 pub variance_ns2: f64,
22}
23
24impl TimeStats {
25 pub fn stddev_ns(&self) -> f64 {
27 self.variance_ns2.sqrt()
28 }
29
30 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}