use std::sync::OnceLock;
use crate::stats::numeric::{floor_p_times_u64, u64_to_f64};
pub const BUCKET_COUNT: usize = 94;
fn bucket_offsets() -> &'static [u64; BUCKET_COUNT] {
static OFFSETS: OnceLock<[u64; BUCKET_COUNT]> = OnceLock::new();
OFFSETS.get_or_init(|| {
let mut offsets = [0u64; BUCKET_COUNT];
let mut last: u64 = 1;
offsets[0] = 1;
for slot in offsets.iter_mut().skip(1) {
let mut next = last.saturating_mul(6) / 5;
if next == last {
next += 1;
}
*slot = next;
last = next;
}
offsets
})
}
#[derive(Clone, Debug)]
pub struct Histogram {
buckets: [u64; BUCKET_COUNT],
val_max: u64,
}
impl Histogram {
pub fn new() -> Self {
Self {
buckets: [0; BUCKET_COUNT],
val_max: 0,
}
}
pub fn record(&mut self, val: u64) {
let offsets = bucket_offsets();
let next = offsets.partition_point(|&o| o <= val);
let bucket = next.saturating_sub(1).min(BUCKET_COUNT - 1);
self.buckets[bucket] = self.buckets[bucket].saturating_add(1);
if val > self.val_max {
self.val_max = val;
}
}
pub const OVERFLOW_SENTINEL: u64 = u64::MAX;
pub fn is_overflowing(&self) -> bool {
self.buckets[BUCKET_COUNT - 1] > 0
}
pub fn count(&self) -> u64 {
self.buckets.iter().copied().fold(0u64, u64::saturating_add)
}
pub fn percentile(&self, p: f64) -> u64 {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return 0;
}
if self.is_overflowing() {
return Self::OVERFLOW_SENTINEL;
}
let total = self.count();
if total == 0 {
return 0;
}
let pcount = floor_p_times_u64(p, total);
if pcount == 0 {
return 0;
}
let offsets = bucket_offsets();
let mut elements: u64 = 0;
for (i, &offset) in offsets.iter().enumerate().take(BUCKET_COUNT - 1) {
elements = elements.saturating_add(self.buckets[i]);
if elements >= pcount {
return offset;
}
}
0
}
pub fn mean(&self) -> f64 {
if self.is_overflowing() {
return f64::INFINITY;
}
let offsets = bucket_offsets();
let mut sum: u64 = 0;
let mut elements: u64 = 0;
for (i, &offset) in offsets.iter().enumerate().take(BUCKET_COUNT - 1) {
elements = elements.saturating_add(self.buckets[i]);
sum = sum.saturating_add(self.buckets[i].saturating_mul(offset));
}
if elements == 0 {
return 0.0;
}
let quotient = sum / elements;
let remainder = sum % elements;
u64_to_f64(quotient) + u64_to_f64(remainder) / u64_to_f64(elements)
}
pub fn max(&self) -> u64 {
if self.is_overflowing() {
return Self::OVERFLOW_SENTINEL;
}
self.val_max
}
pub fn reset(&mut self) {
self.buckets = [0; BUCKET_COUNT];
self.val_max = 0;
}
pub fn merge(&mut self, other: &Self) {
for i in 0..BUCKET_COUNT {
self.buckets[i] = self.buckets[i].saturating_add(other.buckets[i]);
}
if other.val_max > self.val_max {
self.val_max = other.val_max;
}
}
pub fn buckets(&self) -> &[u64; BUCKET_COUNT] {
&self.buckets
}
}
impl Default for Histogram {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offsets_are_strictly_increasing() {
let off = bucket_offsets();
for window in off.windows(2) {
assert!(window[0] < window[1], "offsets must be strictly monotonic");
}
assert_eq!(off[0], 1);
}
#[test]
fn empty_histogram_returns_zeros() {
let h = Histogram::new();
assert_eq!(h.count(), 0);
assert_eq!(h.max(), 0);
assert_eq!(h.percentile(0.5), 0);
assert!(h.mean().abs() < f64::EPSILON);
}
#[test]
fn record_updates_count_and_max() {
let mut h = Histogram::new();
h.record(0);
h.record(5);
h.record(1_000);
assert_eq!(h.count(), 3);
assert_eq!(h.max(), 1_000);
}
#[test]
fn percentile_is_monotone_non_decreasing() {
let mut h = Histogram::new();
for v in 0..1_000 {
h.record(v);
}
let mut last = 0u64;
let mut p = 0.0f64;
while p <= 1.0 {
let v = h.percentile(p);
assert!(v >= last, "percentile decreased at p={p}: {v} < {last}");
last = v;
p += 0.05;
}
}
#[test]
fn merge_sums_counts() {
let mut a = Histogram::new();
let mut b = Histogram::new();
for v in 0..100 {
a.record(v);
b.record(v + 50);
}
let total = a.count() + b.count();
a.merge(&b);
assert_eq!(a.count(), total);
}
#[test]
fn reset_clears_state() {
let mut h = Histogram::new();
h.record(123);
h.reset();
assert_eq!(h.count(), 0);
assert_eq!(h.max(), 0);
}
#[test]
fn percentile_outside_range_returns_zero() {
let mut h = Histogram::new();
h.record(10);
assert_eq!(h.percentile(-0.1), 0);
assert_eq!(h.percentile(1.1), 0);
assert_eq!(h.percentile(f64::NAN), 0);
}
#[test]
fn overflow_signals_quantile_callers() {
let mut h = Histogram::new();
let last_offset = *bucket_offsets().last().expect("non-empty offsets");
h.record(last_offset.saturating_add(1));
assert!(h.is_overflowing(), "expected overflow signal");
assert_eq!(h.percentile(0.5), Histogram::OVERFLOW_SENTINEL);
assert_eq!(h.max(), Histogram::OVERFLOW_SENTINEL);
assert!(h.mean().is_infinite());
h.reset();
assert!(!h.is_overflowing());
}
#[test]
fn mean_rough_match_for_uniform() {
let mut h = Histogram::new();
for v in 0..1000u64 {
h.record(v);
}
let m = h.mean();
assert!((100.0..=1500.0).contains(&m), "mean was {m}");
}
}