Skip to main content

yscv_eval/
timing.rs

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}