1use std::time::{Duration, Instant};
8
9#[derive(Debug, Clone)]
11pub struct TimingSample {
12 pub label: String,
13 pub duration: Duration,
14}
15
16pub fn measure<F: FnOnce()>(label: &str, f: F) -> TimingSample {
18 let start = Instant::now();
19 f();
20 let duration = start.elapsed();
21 TimingSample {
22 label: label.into(),
23 duration,
24 }
25}
26
27pub fn measure_n<F: Fn()>(label: &str, n: usize, f: F) -> Vec<TimingSample> {
29 (0..n)
30 .map(|_| {
31 let start = Instant::now();
32 f();
33 TimingSample {
34 label: label.into(),
35 duration: start.elapsed(),
36 }
37 })
38 .collect()
39}
40
41#[derive(Debug, Clone)]
43pub struct TimingStats {
44 pub count: usize,
45 pub min: Duration,
46 pub max: Duration,
47 pub mean: Duration,
48 pub median: Duration,
49 pub stddev: Duration,
50}
51
52pub fn stats(samples: &[TimingSample]) -> TimingStats {
54 if samples.is_empty() {
55 return TimingStats {
56 count: 0,
57 min: Duration::ZERO,
58 max: Duration::ZERO,
59 mean: Duration::ZERO,
60 median: Duration::ZERO,
61 stddev: Duration::ZERO,
62 };
63 }
64 let durations: Vec<Duration> = samples.iter().map(|s| s.duration).collect();
65 let min = *durations.iter().min().unwrap();
66 let max = *durations.iter().max().unwrap();
67 let total: Duration = durations.iter().sum();
68 let mean = total / samples.len() as u32;
69
70 let mut sorted = durations.clone();
71 sorted.sort();
72 let median = sorted[sorted.len() / 2];
73
74 let variance: f64 = durations
75 .iter()
76 .map(|d| {
77 let diff = d.as_nanos() as f64 - mean.as_nanos() as f64;
78 diff * diff
79 })
80 .sum::<f64>()
81 / samples.len() as f64;
82 let stddev = Duration::from_nanos(variance.sqrt() as u64);
83
84 TimingStats {
85 count: samples.len(),
86 min,
87 max,
88 mean,
89 median,
90 stddev,
91 }
92}
93
94pub fn timing_ratio(group_a: &[TimingSample], group_b: &[TimingSample]) -> f64 {
98 let stats_a = stats(group_a);
99 let stats_b = stats(group_b);
100 let mean_a = stats_a.mean.as_nanos() as f64;
101 let mean_b = stats_b.mean.as_nanos() as f64;
102 if mean_a == 0.0 || mean_b == 0.0 {
103 return 1.0;
104 }
105 let (larger, smaller) = if mean_a > mean_b {
106 (mean_a, mean_b)
107 } else {
108 (mean_b, mean_a)
109 };
110 larger / smaller
111}
112
113pub fn is_constant_time(
115 group_a: &[TimingSample],
116 group_b: &[TimingSample],
117 threshold: f64,
118) -> bool {
119 timing_ratio(group_a, group_b) < threshold
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::thread;
126
127 #[test]
128 fn measure_returns_duration() {
129 let sample = measure("test", || {
130 thread::sleep(Duration::from_micros(100));
131 });
132 assert!(sample.duration >= Duration::from_micros(90));
133 }
134
135 #[test]
136 fn measure_n_returns_multiple() {
137 let samples = measure_n("test", 5, || {});
138 assert_eq!(samples.len(), 5);
139 assert!(samples.iter().all(|s| s.label == "test"));
140 }
141
142 #[test]
143 fn stats_empty_returns_zero() {
144 let s = stats(&[]);
145 assert_eq!(s.count, 0);
146 }
147
148 #[test]
149 fn stats_computes_min_max() {
150 let samples = vec![
151 TimingSample {
152 label: "x".into(),
153 duration: Duration::from_nanos(100),
154 },
155 TimingSample {
156 label: "x".into(),
157 duration: Duration::from_nanos(300),
158 },
159 TimingSample {
160 label: "x".into(),
161 duration: Duration::from_nanos(200),
162 },
163 ];
164 let s = stats(&samples);
165 assert_eq!(s.min, Duration::from_nanos(100));
166 assert_eq!(s.max, Duration::from_nanos(300));
167 }
168
169 #[test]
170 fn stats_computes_mean() {
171 let samples = vec![
172 TimingSample {
173 label: "x".into(),
174 duration: Duration::from_nanos(100),
175 },
176 TimingSample {
177 label: "x".into(),
178 duration: Duration::from_nanos(200),
179 },
180 TimingSample {
181 label: "x".into(),
182 duration: Duration::from_nanos(300),
183 },
184 ];
185 let s = stats(&samples);
186 assert_eq!(s.mean, Duration::from_nanos(200));
187 }
188
189 #[test]
190 fn stats_computes_median() {
191 let samples = vec![
192 TimingSample {
193 label: "x".into(),
194 duration: Duration::from_nanos(100),
195 },
196 TimingSample {
197 label: "x".into(),
198 duration: Duration::from_nanos(200),
199 },
200 TimingSample {
201 label: "x".into(),
202 duration: Duration::from_nanos(300),
203 },
204 TimingSample {
205 label: "x".into(),
206 duration: Duration::from_nanos(400),
207 },
208 TimingSample {
209 label: "x".into(),
210 duration: Duration::from_nanos(500),
211 },
212 ];
213 let s = stats(&samples);
214 assert_eq!(s.median, Duration::from_nanos(300));
215 }
216
217 #[test]
218 fn timing_ratio_equal_groups() {
219 let a = vec![TimingSample {
220 label: "a".into(),
221 duration: Duration::from_nanos(100),
222 }];
223 let b = vec![TimingSample {
224 label: "b".into(),
225 duration: Duration::from_nanos(100),
226 }];
227 assert!((timing_ratio(&a, &b) - 1.0).abs() < 0.01);
228 }
229
230 #[test]
231 fn timing_ratio_different_groups() {
232 let a = vec![TimingSample {
233 label: "a".into(),
234 duration: Duration::from_nanos(100),
235 }];
236 let b = vec![TimingSample {
237 label: "b".into(),
238 duration: Duration::from_nanos(200),
239 }];
240 assert!((timing_ratio(&a, &b) - 2.0).abs() < 0.01);
241 }
242
243 #[test]
244 fn is_constant_time_passes_for_equal() {
245 let a = vec![TimingSample {
246 label: "a".into(),
247 duration: Duration::from_nanos(100),
248 }];
249 let b = vec![TimingSample {
250 label: "b".into(),
251 duration: Duration::from_nanos(105),
252 }];
253 assert!(is_constant_time(&a, &b, 1.5));
254 }
255
256 #[test]
257 fn is_constant_time_fails_for_different() {
258 let a = vec![TimingSample {
259 label: "a".into(),
260 duration: Duration::from_nanos(100),
261 }];
262 let b = vec![TimingSample {
263 label: "b".into(),
264 duration: Duration::from_nanos(500),
265 }];
266 assert!(!is_constant_time(&a, &b, 1.5));
267 }
268}