Skip to main content

keyhog_core/
timing.rs

1//! Shared statistical evidence for paired performance comparisons.
2
3use std::time::Duration;
4
5/// A two-sided 95% confidence interval for a paired candidate/reference ratio.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct PairedRatioConfidence {
8    /// Number of paired observations.
9    pub sample_count: usize,
10    /// Geometric mean of the paired candidate/reference ratios.
11    pub geometric_mean_ratio: f64,
12    /// Lower bound of the two-sided 95% interval.
13    pub low_ratio: f64,
14    /// Upper bound of the two-sided 95% interval.
15    pub high_ratio: f64,
16}
17
18/// Overflow-safe midpoint for two ordered unsigned observations.
19pub fn midpoint_u128(lower: u128, upper: u128) -> u128 {
20    lower + (upper - lower) / 2
21}
22
23/// Compute a paired 95% confidence interval in log-ratio space.
24///
25/// Pairing removes shared trial-to-trial host noise. Every duration must be
26/// positive, the slices must have equal length, and at least two pairs are
27/// required so the interval has measured variance.
28pub fn paired_ratio_confidence_95(
29    reference: &[Duration],
30    candidate: &[Duration],
31) -> Option<PairedRatioConfidence> {
32    if reference.len() != candidate.len() || reference.len() < 2 {
33        return None;
34    }
35    let mut log_ratios = Vec::with_capacity(reference.len());
36    for (&reference_duration, &candidate_duration) in reference.iter().zip(candidate) {
37        let reference_secs = reference_duration.as_secs_f64();
38        let candidate_secs = candidate_duration.as_secs_f64();
39        if reference_secs <= 0.0 || candidate_secs <= 0.0 {
40            return None;
41        }
42        log_ratios.push((candidate_secs / reference_secs).ln());
43    }
44    let count = log_ratios.len() as f64;
45    let mean = log_ratios.iter().sum::<f64>() / count;
46    let variance = log_ratios
47        .iter()
48        .map(|ratio| {
49            let delta = ratio - mean;
50            delta * delta
51        })
52        .sum::<f64>()
53        / (count - 1.0);
54    let half_width = two_sided_95_student_t_critical(log_ratios.len()) * (variance / count).sqrt();
55    Some(PairedRatioConfidence {
56        sample_count: log_ratios.len(),
57        geometric_mean_ratio: mean.exp(),
58        low_ratio: (mean - half_width).exp(),
59        high_ratio: (mean + half_width).exp(),
60    })
61}
62
63/// Median duration using the midpoint of the two central observations for an
64/// even-length sample.
65pub fn median_duration(samples: &[Duration]) -> Option<Duration> {
66    if samples.is_empty() {
67        return None;
68    }
69    let mut sorted = samples.to_vec();
70    sorted.sort_unstable();
71    let middle = sorted.len() / 2;
72    if sorted.len() % 2 == 1 {
73        Some(sorted[middle])
74    } else {
75        let lower = sorted[middle - 1];
76        let upper = sorted[middle];
77        let midpoint_nanos = midpoint_u128(lower.as_nanos(), upper.as_nanos());
78        let seconds = midpoint_nanos / 1_000_000_000;
79        let nanos = (midpoint_nanos % 1_000_000_000) as u32;
80        Some(Duration::new(seconds as u64, nanos))
81    }
82}
83
84/// Conservative two-sided 95% Student-t critical value for a sample count.
85pub fn two_sided_95_student_t_critical(sample_count: usize) -> f64 {
86    match sample_count {
87        0 | 1 => 0.0,
88        2 => 12.706_204_736,
89        3 => 4.302_652_73,
90        4 => 3.182_446_305,
91        5 => 2.776_445_105,
92        6 => 2.570_581_836,
93        7 => 2.446_911_851,
94        8 => 2.364_624_252,
95        9 => 2.306_004_135,
96        10 => 2.262_157_163,
97        11 => 2.228_138_852,
98        12 => 2.200_985_16,
99        13 => 2.178_812_83,
100        14 => 2.160_368_656,
101        15 => 2.144_786_688,
102        16 => 2.131_449_546,
103        17 => 2.119_905_299,
104        18 => 2.109_815_578,
105        19 => 2.100_922_04,
106        20 => 2.093_024_054,
107        21 => 2.085_963_447,
108        22 => 2.079_613_845,
109        23 => 2.073_873_068,
110        24 => 2.068_657_61,
111        25 => 2.063_898_562,
112        26 => 2.059_538_553,
113        27 => 2.055_529_439,
114        28 => 2.051_830_516,
115        29 => 2.048_407_142,
116        30 => 2.045_229_642,
117        31 => 2.042_272_456,
118        _ => 2.042_272_456,
119    }
120}