1use std::time::Duration;
2
3use crate::EvalError;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct TimingStats {
7 pub runs: usize,
8 pub total_ms: f64,
9 pub mean_ms: f64,
10 pub min_ms: f64,
11 pub max_ms: f64,
12 pub p50_ms: f64,
13 pub p95_ms: f64,
14 pub throughput_fps: f64,
15}
16
17pub fn summarize_durations(durations: &[Duration]) -> Result<TimingStats, EvalError> {
18 if durations.is_empty() {
19 return Err(EvalError::EmptyDurationSeries);
20 }
21
22 let mut ms_values = durations
23 .iter()
24 .map(|duration| duration.as_secs_f64() * 1_000.0)
25 .collect::<Vec<_>>();
26 ms_values.sort_by(|left, right| left.total_cmp(right));
27
28 let runs = ms_values.len();
29 let total_ms = ms_values.iter().copied().sum::<f64>();
30 let min_ms = ms_values[0];
31 let max_ms = ms_values[runs - 1];
32 let mean_ms = total_ms / runs as f64;
33 let p50_ms = percentile(&ms_values, 50.0);
34 let p95_ms = percentile(&ms_values, 95.0);
35 let throughput_fps = if total_ms <= 0.0 {
36 0.0
37 } else {
38 runs as f64 / (total_ms / 1_000.0)
39 };
40
41 Ok(TimingStats {
42 runs,
43 total_ms,
44 mean_ms,
45 min_ms,
46 max_ms,
47 p50_ms,
48 p95_ms,
49 throughput_fps,
50 })
51}
52
53fn percentile(sorted_ms: &[f64], percentile: f64) -> f64 {
54 if sorted_ms.is_empty() {
55 return 0.0;
56 }
57 if sorted_ms.len() == 1 {
58 return sorted_ms[0];
59 }
60
61 let clamped = percentile.clamp(0.0, 100.0) / 100.0;
62 let max_index = (sorted_ms.len() - 1) as f64;
63 let rank = clamped * max_index;
64 let lower = rank.floor() as usize;
65 let upper = rank.ceil() as usize;
66 if lower == upper {
67 sorted_ms[lower]
68 } else {
69 let weight = rank - lower as f64;
70 sorted_ms[lower] * (1.0 - weight) + sorted_ms[upper] * weight
71 }
72}