pub const MIN_PERCENTILE_SAMPLES: usize = 20;
#[must_use]
pub fn percentile_of(sorted: &[u64], pct: usize) -> Option<u64> {
let rank = (pct * sorted.len()).div_ceil(100).max(1);
sorted.get(rank - 1).copied()
}
#[derive(Clone, Debug)]
pub struct Window<const N: usize> {
samples: std::collections::VecDeque<u64>,
}
impl<const N: usize> Default for Window<N> {
fn default() -> Self {
Self {
samples: std::collections::VecDeque::with_capacity(N),
}
}
}
impl<const N: usize> Window<N> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, sample: u64) {
if self.samples.len() >= N {
self.samples.pop_front();
}
if N > 0 {
self.samples.push_back(sample);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
#[must_use]
pub fn percentiles(&self) -> Option<(u64, u64)> {
if self.samples.len() < MIN_PERCENTILE_SAMPLES {
return None;
}
let mut sorted = [0u64; N];
let sorted = &mut sorted[..self.samples.len()];
for (slot, sample) in sorted.iter_mut().zip(&self.samples) {
*slot = *sample;
}
sorted.sort_unstable();
Some((percentile_of(sorted, 50)?, percentile_of(sorted, 95)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_percentile_is_the_sample_at_the_nearest_rank() {
let sorted: Vec<u64> = (1..=20).collect();
assert_eq!(percentile_of(&sorted, 50), Some(10));
assert_eq!(percentile_of(&sorted, 95), Some(19));
assert_eq!(percentile_of(&sorted, 99), Some(20));
assert_eq!(percentile_of(&sorted, 100), Some(20));
assert_eq!(percentile_of(&sorted, 1), Some(1));
assert_eq!(percentile_of(&[7], 50), Some(7));
assert_eq!(percentile_of(&[7], 1), Some(7));
assert_eq!(percentile_of(&[], 50), None);
}
#[test]
fn the_window_sorts_before_it_picks() {
let mut ascending = Window::<120>::new();
let mut descending = Window::<120>::new();
for value in 1..=MIN_PERCENTILE_SAMPLES as u64 {
ascending.record(value);
}
for value in (1..=MIN_PERCENTILE_SAMPLES as u64).rev() {
descending.record(value);
}
assert_eq!(ascending.percentiles(), Some((10, 19)));
assert_eq!(descending.percentiles(), ascending.percentiles());
}
#[test]
fn a_short_window_has_no_percentile() {
let mut window = Window::<120>::new();
assert!(window.is_empty());
assert_eq!(window.percentiles(), None);
for _ in 0..MIN_PERCENTILE_SAMPLES - 1 {
window.record(7);
}
assert_eq!(window.len(), MIN_PERCENTILE_SAMPLES - 1);
assert_eq!(window.percentiles(), None, "one short is still short");
window.record(7);
assert_eq!(window.percentiles(), Some((7, 7)));
}
#[test]
fn the_window_evicts_the_oldest() {
const N: usize = 24;
let mut window = Window::<N>::new();
for _ in 0..N {
window.record(1_000);
}
assert_eq!(window.len(), N);
assert_eq!(window.percentiles(), Some((1_000, 1_000)));
for _ in 0..N {
window.record(1);
}
assert_eq!(window.len(), N, "the window does not grow past its bound");
assert_eq!(window.percentiles(), Some((1, 1)));
}
#[test]
fn the_minimum_is_the_first_count_at_which_p95_stops_being_the_maximum() {
for count in 1..MIN_PERCENTILE_SAMPLES {
let sorted: Vec<u64> = (1..=count as u64).collect();
assert_eq!(
percentile_of(&sorted, 95),
percentile_of(&sorted, 100),
"at {count} samples a p95 is still the maximum"
);
}
let sorted: Vec<u64> = (1..=MIN_PERCENTILE_SAMPLES as u64).collect();
assert_ne!(
percentile_of(&sorted, 95),
percentile_of(&sorted, 100),
"at {MIN_PERCENTILE_SAMPLES} samples they must part company"
);
}
}