Skip to main content

subms_stats/
samples.rs

1//! Ergonomic wrapper around a borrowed `&[u64]` slice of nanosecond
2//! latency samples. Method-chained API for the common statistics.
3//!
4//! The set of methods available on `SubMsSamples` depends on which
5//! Cargo features are enabled. Always-on: `count`, `is_empty`, `raw`,
6//! `p50` / `p99` / `p999` / `percentile` / `percentile_sweep`,
7//! `mean`, `stddev`, `max`. Feature-gated methods are documented at
8//! their definitions.
9//!
10//! ```
11//! use subms_stats::SubMsSamples;
12//!
13//! let raw = vec![100u64, 200, 150, 300, 250, 175, 125, 400];
14//! let s = SubMsSamples::new(&raw);
15//! let p50 = s.p50();
16//! let p99 = s.p99();
17//! assert!(p99 >= p50);
18//! ```
19
20use crate::percentiles;
21
22/// Borrowing facade over a sample slice. Zero-cost - holds a
23/// reference only. Lazy-sorts internally on demand for percentile
24/// queries.
25#[derive(Clone, Copy)]
26pub struct SubMsSamples<'a> {
27    raw: &'a [u64],
28}
29
30impl<'a> SubMsSamples<'a> {
31    /// Wrap a borrowed slice of nanosecond samples.
32    pub fn new(raw: &'a [u64]) -> Self {
33        Self { raw }
34    }
35
36    /// Number of samples in the underlying slice.
37    pub fn count(&self) -> usize {
38        self.raw.len()
39    }
40
41    /// `true` if the underlying slice is empty.
42    pub fn is_empty(&self) -> bool {
43        self.raw.is_empty()
44    }
45
46    /// Raw chronological sample buffer.
47    pub fn raw(&self) -> &'a [u64] {
48        self.raw
49    }
50
51    /// 50th percentile (median).
52    pub fn p50(&self) -> u64 {
53        self.percentile(0.50)
54    }
55
56    /// 90th percentile.
57    pub fn p90(&self) -> u64 {
58        self.percentile(0.90)
59    }
60
61    /// 99th percentile - the standard tail-latency headline.
62    pub fn p99(&self) -> u64 {
63        self.percentile(0.99)
64    }
65
66    /// 99.9th percentile - the "worst typical case".
67    pub fn p999(&self) -> u64 {
68        self.percentile(0.999)
69    }
70
71    /// Maximum observed sample, `0` if empty.
72    pub fn max(&self) -> u64 {
73        self.raw.iter().copied().max().unwrap_or(0)
74    }
75
76    /// Arithmetic mean. `0` if empty.
77    pub fn mean(&self) -> u64 {
78        percentiles::mean(self.raw)
79    }
80
81    /// Sample standard deviation (n-1).
82    pub fn stddev(&self) -> u64 {
83        percentiles::stddev(self.raw)
84    }
85
86    /// Arbitrary quantile in `[0.0, 1.0]`. Sorts internally.
87    pub fn percentile(&self, q: f64) -> u64 {
88        let mut sorted = self.raw.to_vec();
89        sorted.sort_unstable();
90        percentiles::percentile(&sorted, q)
91    }
92
93    /// Sweep across a quantile range.
94    pub fn percentile_sweep(&self, start: f64, end: f64, step: f64) -> Vec<(f64, u64)> {
95        percentiles::percentile_sweep(self.raw, start, end, step)
96    }
97
98    /// Log2-spaced CDF buckets. Only available with the `histogram`
99    /// feature.
100    #[cfg(feature = "histogram")]
101    pub fn cdf_buckets(&self) -> Vec<u64> {
102        crate::histogram::cdf_buckets(self.raw)
103    }
104
105    /// Measurement-rig jitter score in `[0.0, 1.0]`. Only available
106    /// with the `jitter` feature.
107    #[cfg(feature = "jitter")]
108    pub fn jitter_score(&self) -> f64 {
109        crate::jitter::jitter_score(self.raw)
110    }
111
112    /// Conditional tail expectation. Only with `tail` feature.
113    #[cfg(feature = "tail")]
114    pub fn conditional_tail_expectation(&self, q: f64) -> u64 {
115        crate::tail::conditional_tail_expectation(self.raw, q)
116    }
117
118    /// Tail fatness ratio (p99 / p50). Only with `tail` feature.
119    #[cfg(feature = "tail")]
120    pub fn tail_fatness_ratio(&self) -> f64 {
121        crate::tail::tail_fatness_ratio(self.raw)
122    }
123
124    /// Hill estimator. Only with `tail` feature.
125    #[cfg(feature = "tail")]
126    pub fn hill_tail_index(&self, k: usize) -> Option<f64> {
127        crate::tail::hill_tail_index(self.raw, k)
128    }
129
130    /// Interquartile range. Only with `robust` feature.
131    #[cfg(feature = "robust")]
132    pub fn iqr(&self) -> u64 {
133        crate::robust::iqr(self.raw)
134    }
135
136    /// Median absolute deviation. Only with `robust` feature.
137    #[cfg(feature = "robust")]
138    pub fn median_absolute_deviation(&self) -> u64 {
139        crate::robust::median_absolute_deviation(self.raw)
140    }
141
142    /// Coefficient of variation. Only with `robust` feature.
143    #[cfg(feature = "robust")]
144    pub fn coefficient_of_variation(&self) -> f64 {
145        crate::robust::coefficient_of_variation(self.raw)
146    }
147
148    /// Skewness (3rd standardised moment). Only with `robust` feature.
149    #[cfg(feature = "robust")]
150    pub fn skewness(&self) -> f64 {
151        crate::robust::skewness(self.raw)
152    }
153
154    /// Excess kurtosis. Only with `robust` feature.
155    #[cfg(feature = "robust")]
156    pub fn kurtosis(&self) -> f64 {
157        crate::robust::kurtosis(self.raw)
158    }
159
160    /// Bootstrap CI for a percentile. Only with `bootstrap` feature.
161    #[cfg(feature = "bootstrap")]
162    pub fn bootstrap_percentile_ci(
163        &self,
164        q: f64,
165        iters: usize,
166        confidence: f64,
167        seed: u64,
168    ) -> (u64, u64) {
169        crate::bootstrap::bootstrap_percentile_ci(self.raw, q, iters, confidence, seed)
170    }
171}
172
173impl<'a> From<&'a [u64]> for SubMsSamples<'a> {
174    fn from(raw: &'a [u64]) -> Self {
175        Self::new(raw)
176    }
177}
178
179impl<'a> From<&'a Vec<u64>> for SubMsSamples<'a> {
180    fn from(raw: &'a Vec<u64>) -> Self {
181        Self::new(raw.as_slice())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn empty_samples_returns_zero() {
191        let raw: Vec<u64> = Vec::new();
192        let s = SubMsSamples::new(&raw);
193        assert_eq!(s.count(), 0);
194        assert!(s.is_empty());
195        assert_eq!(s.p99(), 0);
196        assert_eq!(s.mean(), 0);
197        assert_eq!(s.stddev(), 0);
198        assert_eq!(s.max(), 0);
199    }
200
201    #[test]
202    fn known_distribution_percentiles() {
203        let raw: Vec<u64> = (0..100).collect();
204        let s = SubMsSamples::new(&raw);
205        assert_eq!(s.count(), 100);
206        assert_eq!(s.p50(), 50);
207        assert_eq!(s.p99(), 99);
208        assert_eq!(s.max(), 99);
209    }
210
211    #[test]
212    fn from_slice_and_vec_both_work() {
213        let v: Vec<u64> = vec![100, 200, 300];
214        let by_slice: SubMsSamples<'_> = (&v[..]).into();
215        let by_vec: SubMsSamples<'_> = (&v).into();
216        assert_eq!(by_slice.p50(), by_vec.p50());
217    }
218}