use serde::Serialize;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct Distribution {
pub count: usize,
pub mean_us: u32,
pub p50_us: u32,
pub p95_us: u32,
pub p99_us: u32,
pub max_us: u32,
pub over_budget: usize,
}
impl Distribution {
pub fn of(values: &mut [u32], budget_us: u32) -> Self {
if values.is_empty() {
return Self::default();
}
values.sort_unstable();
let count = values.len();
let total: u64 = values.iter().map(|v| u64::from(*v)).sum();
Self {
count,
mean_us: (total / count as u64) as u32,
p50_us: nearest_rank(values, 0.50),
p95_us: nearest_rank(values, 0.95),
p99_us: nearest_rank(values, 0.99),
max_us: values[count - 1],
over_budget: values.iter().filter(|v| **v > budget_us).count(),
}
}
pub fn over_budget_share(&self) -> f32 {
if self.count == 0 {
return 0.0;
}
self.over_budget as f32 / self.count as f32
}
}
fn nearest_rank(sorted: &[u32], q: f32) -> u32 {
let rank = (q * sorted.len() as f32).ceil() as usize;
sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
}
pub(super) fn mean_u32(values: impl Iterator<Item = u32>) -> u32 {
let mut total: u64 = 0;
let mut count: u64 = 0;
for v in values {
total += u64::from(v);
count += 1;
}
total.checked_div(count).unwrap_or(0) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_column_summarizes_to_nothing() {
let d = Distribution::of(&mut [], 16_667);
assert_eq!(d, Distribution::default());
assert_eq!(d.over_budget_share(), 0.0);
}
#[test]
fn a_single_sample_is_every_percentile() {
let d = Distribution::of(&mut [500], 16_667);
assert_eq!((d.count, d.mean_us, d.max_us), (1, 500, 500));
assert_eq!((d.p50_us, d.p95_us, d.p99_us), (500, 500, 500));
}
#[test]
fn percentiles_are_samples_that_actually_happened() {
let mut values: Vec<u32> = (1..=100).collect();
let d = Distribution::of(&mut values, 1_000);
assert_eq!(d.p50_us, 50);
assert_eq!(d.p95_us, 95);
assert_eq!(d.p99_us, 99);
assert_eq!(d.max_us, 100);
assert_eq!(d.mean_us, 50);
}
#[test]
fn the_summary_does_not_depend_on_the_order_it_arrives_in() {
let mut ascending: Vec<u32> = (1..=50).collect();
let mut descending: Vec<u32> = (1..=50).rev().collect();
assert_eq!(
Distribution::of(&mut ascending, 25),
Distribution::of(&mut descending, 25)
);
}
#[test]
fn the_budget_counts_frames_over_it_and_not_frames_on_it() {
let mut values = vec![10, 20, 20, 21, 40];
let d = Distribution::of(&mut values, 20);
assert_eq!(d.over_budget, 2);
assert!((d.over_budget_share() - 0.4).abs() < 1e-6);
}
#[test]
fn a_tail_that_a_mean_would_hide_shows_up_in_the_percentiles() {
let mut values = vec![10_000; 99];
values.push(100_000);
let d = Distribution::of(&mut values, 16_667);
assert!(d.mean_us < 11_000, "{}", d.mean_us);
assert_eq!(d.p50_us, 10_000);
assert_eq!(d.max_us, 100_000);
assert_eq!(d.over_budget, 1);
}
#[test]
fn the_mean_helper_folds_an_empty_iterator_to_zero() {
assert_eq!(mean_u32(core::iter::empty()), 0);
assert_eq!(mean_u32([2, 4, 6].into_iter()), 4);
}
}