Skip to main content

fluxbench_stats/
percentiles.rs

1//! Percentile Computation
2//!
3//! Computes percentiles from raw samples (NOT cleaned data).
4//! Tail latency percentiles (p99, p999) must include outliers as they ARE the signal.
5
6/// Standard percentiles to compute
7#[derive(Debug, Clone)]
8pub struct Percentiles {
9    /// 50th percentile (median)
10    pub p50: f64,
11    /// 75th percentile
12    pub p75: f64,
13    /// 90th percentile
14    pub p90: f64,
15    /// 95th percentile
16    pub p95: f64,
17    /// 99th percentile
18    pub p99: f64,
19    /// 99.9th percentile
20    pub p999: f64,
21}
22
23/// Compute a single percentile from samples
24///
25/// Uses linear interpolation between nearest ranks.
26///
27/// # Examples
28///
29/// ```ignore
30/// # use fluxbench_stats::compute_percentile;
31/// let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0];
32/// let p50 = compute_percentile(&samples, 50.0);  // Median
33/// let p95 = compute_percentile(&samples, 95.0);  // 95th percentile
34/// println!("Median: {}", p50);
35/// println!("P95: {}", p95);
36/// ```
37pub fn compute_percentile(samples: &[f64], percentile: f64) -> f64 {
38    if samples.is_empty() {
39        return 0.0;
40    }
41
42    if samples.len() == 1 {
43        return samples[0];
44    }
45
46    let mut sorted = samples.to_vec();
47    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
48
49    let n = sorted.len();
50    let p = percentile / 100.0;
51
52    // Linear interpolation between nearest ranks
53    let rank = p * (n - 1) as f64;
54    let lower_idx = rank.floor() as usize;
55    let upper_idx = (lower_idx + 1).min(n - 1);
56    let fraction = rank - lower_idx as f64;
57
58    sorted[lower_idx] + fraction * (sorted[upper_idx] - sorted[lower_idx])
59}
60
61/// Compute all standard percentiles
62pub fn compute_percentiles(samples: &[f64]) -> Percentiles {
63    Percentiles {
64        p50: compute_percentile(samples, 50.0),
65        p75: compute_percentile(samples, 75.0),
66        p90: compute_percentile(samples, 90.0),
67        p95: compute_percentile(samples, 95.0),
68        p99: compute_percentile(samples, 99.0),
69        p999: compute_percentile(samples, 99.9),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_median() {
79        let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0];
80        let p50 = compute_percentile(&samples, 50.0);
81        assert!((p50 - 3.0).abs() < 0.01);
82    }
83
84    #[test]
85    fn test_quartiles() {
86        let samples: Vec<f64> = (1..=100).map(|x| x as f64).collect();
87        let p25 = compute_percentile(&samples, 25.0);
88        let p75 = compute_percentile(&samples, 75.0);
89
90        assert!((p25 - 25.75).abs() < 1.0);
91        assert!((p75 - 75.25).abs() < 1.0);
92    }
93
94    #[test]
95    fn test_extreme_percentiles() {
96        let samples: Vec<f64> = (1..=1000).map(|x| x as f64).collect();
97        let p99 = compute_percentile(&samples, 99.0);
98        let p999 = compute_percentile(&samples, 99.9);
99
100        assert!(p99 > 985.0 && p99 < 995.0);
101        assert!(p999 > 998.0 && p999 <= 1000.0);
102    }
103
104    #[test]
105    fn test_single_sample() {
106        let samples = vec![42.0];
107        let p50 = compute_percentile(&samples, 50.0);
108        assert!((p50 - 42.0).abs() < f64::EPSILON);
109    }
110
111    #[test]
112    fn test_empty_samples() {
113        let samples: Vec<f64> = Vec::new();
114        let p50 = compute_percentile(&samples, 50.0);
115        assert!((p50 - 0.0).abs() < f64::EPSILON);
116    }
117
118    #[test]
119    fn test_compute_all_percentiles() {
120        let samples: Vec<f64> = (1..=100).map(|x| x as f64).collect();
121        let percentiles = compute_percentiles(&samples);
122
123        assert!(percentiles.p50 > 49.0 && percentiles.p50 < 51.0);
124        assert!(percentiles.p90 > 89.0 && percentiles.p90 < 91.0);
125        assert!(percentiles.p99 > 98.0 && percentiles.p99 < 100.0);
126    }
127}