use super::bucket_ref::BucketRef;
use super::log_scale::LOG_SCALE;
use super::log_scale::LogScale;
use super::percentile_stats::PercentileStats;
use super::slot::Slot;
use super::slot_queue::SlotQueue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Histogram<T = (), const WIDTH: usize = 3> {
log_scale: &'static LogScale<WIDTH>,
slots: SlotQueue<T>,
aggregate_buckets: Vec<u64>,
}
impl<T> Default for Histogram<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Histogram<T> {
pub fn new() -> Self {
Self::with_slots(1)
}
pub fn with_slots(slot_limit: usize) -> Self {
Self::with_log_scale(&LOG_SCALE, slot_limit)
}
}
impl<T, const WIDTH: usize> Histogram<T, WIDTH> {
pub fn with_log_scale(log_scale: &'static LogScale<WIDTH>, slot_limit: usize) -> Self {
let num_buckets = log_scale.num_buckets();
Self {
log_scale,
slots: SlotQueue::new(slot_limit, num_buckets),
aggregate_buckets: vec![0; num_buckets],
}
}
pub fn record(&mut self, value: u64) {
self.record_n(value, 1);
}
pub fn record_n(&mut self, value: u64, count: u64) {
let bucket_index = self.log_scale.calculate_bucket(value);
if self.slots.slot_limit() > 1 {
self.slots.back_mut().unwrap().buckets[bucket_index] += count;
}
self.aggregate_buckets[bucket_index] += count;
}
#[allow(dead_code)]
pub fn advance(&mut self, data: T) -> usize {
if self.slots.slot_limit() <= 1 {
self.aggregate_buckets.fill(0);
return 0;
}
if self.slots.len() == self.slots.slot_limit() {
let evicted = self.slots.pop_front().unwrap();
for (i, &count) in evicted.buckets.iter().enumerate() {
self.aggregate_buckets[i] -= count;
}
}
let mut slot = Slot::new(self.log_scale.num_buckets());
slot.data = Some(data);
self.slots.push_back(slot);
self.slots.len()
}
#[allow(dead_code)]
#[inline]
pub fn active_slot_count(&self) -> usize {
self.slots.len()
}
#[allow(dead_code)]
#[inline]
pub fn slot_limit(&self) -> usize {
self.slots.slot_limit()
}
#[cfg(test)]
#[inline]
pub(crate) fn slot(&self, index: usize) -> Option<&Slot<T>> {
self.slots.get(index)
}
#[cfg(test)]
#[inline]
pub(crate) fn current_slot(&self) -> &Slot<T> {
self.slots.back().unwrap()
}
pub fn total(&self) -> u64 {
self.aggregate_buckets.iter().sum()
}
#[allow(dead_code)]
pub fn percentile(&self, p: f64) -> u64 {
let total = self.total();
self.percentile_with_total(p, total)
}
fn percentile_with_total(&self, p: f64, total: u64) -> u64 {
if total == 0 {
return 0;
}
let target = (total as f64 * p).ceil().max(1.0) as u64;
let mut cumulative = 0u64;
for (bucket_index, &count) in self.aggregate_buckets.iter().enumerate() {
let prev_cumulative = cumulative;
cumulative += count;
if cumulative >= target {
let prev_count = if bucket_index > 0 {
self.aggregate_buckets[bucket_index - 1]
} else {
0
};
let next_count = self.aggregate_buckets.get(bucket_index + 1).copied().unwrap_or(0);
return self.log_scale.interpolate(
bucket_index,
target - prev_cumulative,
count,
prev_count,
next_count,
);
}
}
0
}
pub fn percentile_stats(&self) -> PercentileStats {
let samples = self.total();
PercentileStats {
samples,
p0_1: self.percentile_with_total(0.001, samples),
p1: self.percentile_with_total(0.01, samples),
p5: self.percentile_with_total(0.05, samples),
p10: self.percentile_with_total(0.10, samples),
p50: self.percentile_with_total(0.50, samples),
p90: self.percentile_with_total(0.90, samples),
p99: self.percentile_with_total(0.99, samples),
p99_9: self.percentile_with_total(0.999, samples),
}
}
pub fn bucket_data(&self) -> impl Iterator<Item = BucketRef<'_, WIDTH>> + '_ {
(0..self.num_buckets()).map(|i| self.bucket(i))
}
#[cfg(test)]
pub(crate) fn get_bucket(&self, index: usize) -> u64 {
self.aggregate_buckets[index]
}
pub const fn total_buckets() -> usize {
LogScale::<WIDTH>::total_buckets()
}
pub const fn num_buckets(&self) -> usize {
Self::total_buckets()
}
pub fn bucket(&self, index: usize) -> BucketRef<'_, WIDTH> {
BucketRef::new(self.log_scale, index, self.aggregate_buckets[index])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::histogram::LogScale3;
use crate::histogram::LogScaleConfig;
#[test]
fn test_bucket_data() {
let mut hist: Histogram = Histogram::new();
hist.record(5);
hist.record_n(10, 3);
let non_empty: Vec<_> = hist.bucket_data().filter(|b| b.count() > 0).collect();
assert_eq!(non_empty.len(), 2);
assert_eq!(
(non_empty[0].left(), non_empty[0].right(), non_empty[0].count()),
(5, 6, 1)
);
assert_eq!(
(non_empty[1].left(), non_empty[1].right(), non_empty[1].count()),
(10, 12, 3)
);
}
#[test]
fn test_bucket_data_includes_all_buckets() {
let hist: Histogram = Histogram::new();
assert_eq!(hist.bucket_data().count(), 252);
assert!(hist.bucket_data().all(|b| b.count() == 0));
}
#[test]
fn test_slot_clear() {
let mut slot: Slot<String> = Slot::new(10);
slot.buckets[0] = 5;
slot.buckets[5] = 10;
slot.data = Some("test".to_string());
slot.clear();
assert!(slot.buckets.iter().all(|&c| c == 0));
assert_eq!(slot.data, None);
}
#[test]
fn test_histogram_default() {
let hist: Histogram = Histogram::default();
assert_eq!(hist.slot_limit(), 1);
assert_eq!(hist.active_slot_count(), 0);
assert_eq!(hist.total(), 0);
}
#[test]
fn test_record_and_total() {
let mut hist: Histogram = Histogram::new();
hist.record(1);
hist.record(5);
hist.record(10);
hist.record(100);
assert_eq!(hist.total(), 4);
assert_eq!(hist.get_bucket(1), 1);
assert_eq!(hist.get_bucket(5), 1);
assert_eq!(hist.get_bucket(LogScale3::calculate_bucket_uncached(10)), 1);
assert_eq!(hist.get_bucket(LogScale3::calculate_bucket_uncached(100)), 1);
}
#[test]
fn test_record_same_bucket() {
let mut hist: Histogram = Histogram::new();
hist.record(8);
hist.record(8);
hist.record(8);
assert_eq!(hist.total(), 3);
assert_eq!(hist.get_bucket(8), 3);
}
#[test]
fn test_record_n() {
let mut hist: Histogram = Histogram::new();
hist.record_n(10, 5);
hist.record_n(100, 3);
assert_eq!(hist.total(), 8);
assert_eq!(hist.get_bucket(LogScale3::calculate_bucket_uncached(10)), 5);
assert_eq!(hist.get_bucket(LogScale3::calculate_bucket_uncached(100)), 3);
}
#[test]
fn test_record_n_equivalent_to_record() {
let mut hist_n: Histogram = Histogram::new();
let mut hist_single: Histogram = Histogram::new();
hist_n.record_n(42, 4);
for _ in 0..4 {
hist_single.record(42);
}
assert_eq!(hist_n.total(), hist_single.total());
let bucket = LogScale3::calculate_bucket_uncached(42);
assert_eq!(hist_n.get_bucket(bucket), hist_single.get_bucket(bucket));
}
#[test]
fn test_u64_max_coverage() {
let max_bucket = LogScale3::calculate_bucket_uncached(u64::MAX);
assert_eq!(max_bucket, 251, "u64::MAX should map to bucket 251");
assert_eq!(LogScaleConfig::<3>::BUCKETS, 252, "Should need exactly 252 buckets");
let mut hist: Histogram = Histogram::new();
assert_eq!(hist.num_buckets(), 252);
hist.record(u64::MAX);
assert_eq!(hist.get_bucket(251), 1);
assert_eq!(hist.total(), 1);
}
#[test]
fn test_percentile_empty() {
let hist: Histogram = Histogram::new();
assert_eq!(hist.percentile(0.5), 0);
assert_eq!(hist.percentile_stats(), PercentileStats {
samples: 0,
p0_1: 0,
p1: 0,
p5: 0,
p10: 0,
p50: 0,
p90: 0,
p99: 0,
p99_9: 0
});
}
#[test]
fn test_percentile_single_value() {
let mut hist: Histogram = Histogram::new();
hist.record(10);
assert_eq!(hist.percentile(0.0), 11);
assert_eq!(hist.percentile(0.5), 11);
assert_eq!(hist.percentile(0.99), 11);
assert_eq!(hist.percentile(1.0), 11);
}
#[test]
fn test_percentile_multiple_values() {
let mut hist: Histogram = Histogram::new();
for value in 1..=10 {
for _ in 0..10 {
hist.record(value);
}
}
assert_eq!(hist.total(), 100);
let p50 = hist.percentile(0.5);
assert!((4..=6).contains(&p50), "P50 = {p50}");
let p90 = hist.percentile(0.9);
assert!((8..=10).contains(&p90), "P90 = {p90}");
let p99 = hist.percentile(0.99);
assert!((9..=11).contains(&p99), "P99 = {p99}");
}
#[test]
fn test_percentile_stats() {
let mut hist: Histogram = Histogram::new();
for i in 1..=100 {
hist.record(i);
}
let stats = hist.percentile_stats();
assert!(stats.p50 >= 48 && stats.p50 <= 52, "P50 = {}", stats.p50);
assert!(stats.p90 >= 80 && stats.p90 <= 92, "P90 = {}", stats.p90);
assert!(stats.p99 >= 96 && stats.p99 <= 112, "P99 = {}", stats.p99);
}
#[test]
fn test_percentile_large_values() {
let mut hist: Histogram = Histogram::new();
hist.record(1);
hist.record(10);
hist.record(100);
hist.record(1000);
hist.record(10000);
assert_eq!(hist.total(), 5);
let p50 = hist.percentile(0.5);
assert!((96..=104).contains(&p50), "P50 = {p50}");
let p80 = hist.percentile(0.8);
assert!((896..=1000).contains(&p80), "P80 = {p80}");
}
#[test]
fn test_with_slots_creates_correct_slot_limit() {
let hist: Histogram<u64> = Histogram::with_slots(4);
assert_eq!(hist.slot_limit(), 4);
assert_eq!(hist.active_slot_count(), 1);
}
#[test]
fn test_advance_single_slot() {
let mut hist: Histogram<u64> = Histogram::new();
hist.record(42);
assert_eq!(hist.total(), 1);
assert_eq!(hist.advance(10), 0);
assert_eq!(hist.active_slot_count(), 0);
assert_eq!(hist.total(), 0);
}
#[test]
fn test_advance_multi_slot_not_full() {
let mut hist: Histogram<u64> = Histogram::with_slots(4);
hist.record(100);
assert_eq!(hist.active_slot_count(), 1);
assert_eq!(hist.total(), 1);
assert_eq!(hist.advance(10), 2);
hist.record(200);
assert_eq!(hist.active_slot_count(), 2);
assert_eq!(hist.total(), 2);
assert_eq!(hist.current_slot().data, Some(10));
assert_eq!(hist.advance(20), 3);
assert_eq!(hist.active_slot_count(), 3);
assert_eq!(hist.advance(30), 4);
assert_eq!(hist.active_slot_count(), 4);
}
#[test]
fn test_advance_evicts_oldest() {
let mut hist: Histogram<u64> = Histogram::with_slots(4);
hist.record(100); hist.advance(10); hist.record(200); hist.advance(20); hist.advance(30);
assert_eq!(hist.active_slot_count(), 4);
assert_eq!(hist.total(), 2);
assert_eq!(hist.advance(40), 4);
assert_eq!(hist.active_slot_count(), 4);
assert_eq!(hist.total(), 1);
assert_eq!(hist.slot(0).unwrap().data, Some(10));
assert_eq!(hist.current_slot().data, Some(40));
}
#[test]
fn test_advance_slot_limit_stays_constant() {
let mut hist: Histogram<u64> = Histogram::with_slots(3);
assert_eq!(hist.slot_limit(), 3);
hist.advance(1);
hist.advance(2);
assert_eq!(hist.active_slot_count(), 3);
assert_eq!(hist.slot_limit(), 3);
for i in 3..10 {
hist.advance(i);
assert_eq!(hist.slot_limit(), 3, "slot limit grew unexpectedly at iteration {i}");
assert_eq!(hist.active_slot_count(), 3);
}
assert_eq!(hist.slot(0).unwrap().data, Some(7));
assert_eq!(hist.slot(1).unwrap().data, Some(8));
assert_eq!(hist.slot(2).unwrap().data, Some(9));
}
#[test]
fn test_advance_uses_slot_limit_instead_of_vecdeque_capacity() {
let mut hist: Histogram<u64> = Histogram::with_slots(2);
hist.slots.reserve(16);
assert!(std::ops::Deref::deref(&hist.slots).capacity() > hist.slot_limit());
hist.advance(1);
assert_eq!(hist.active_slot_count(), 2);
hist.advance(2);
assert_eq!(hist.active_slot_count(), 2);
assert_eq!(hist.slot(0).unwrap().data, Some(1));
assert_eq!(hist.current_slot().data, Some(2));
}
#[test]
fn test_slot_data_access() {
let mut hist: Histogram<String> = Histogram::with_slots(3);
assert_eq!(hist.slot(0).unwrap().data, None);
hist.advance("first".to_string());
assert_eq!(hist.active_slot_count(), 2);
assert_eq!(hist.current_slot().data, Some("first".to_string()));
hist.advance("second".to_string());
assert_eq!(hist.active_slot_count(), 3);
assert_eq!(hist.current_slot().data, Some("second".to_string()));
}
#[test]
fn test_percentile_across_slots() {
let mut hist: Histogram<u64> = Histogram::with_slots(4);
for v in 1..=50 {
hist.record(v);
}
hist.advance(1);
for v in 51..=100 {
hist.record(v);
}
assert_eq!(hist.total(), 100);
let p50 = hist.percentile(0.5);
assert!((48..=52).contains(&p50), "P50 = {p50}");
}
#[test]
fn test_with_slots_zero_same_as_one() {
let mut hist0: Histogram = Histogram::with_slots(0);
let mut hist1: Histogram = Histogram::with_slots(1);
assert_eq!(hist0.active_slot_count(), 0);
assert_eq!(hist1.active_slot_count(), 0);
hist0.record(100);
hist1.record(100);
assert_eq!(hist0.total(), hist1.total());
assert_eq!(hist0.percentile(0.5), hist1.percentile(0.5));
}
#[test]
fn test_aggregate_buckets_consistency() {
let mut hist: Histogram<u64> = Histogram::with_slots(3);
for v in [1, 10, 100, 1000] {
hist.record(v);
}
let manual_total = |h: &Histogram<u64>| -> u64 {
(0..h.active_slot_count()).flat_map(|i| h.slot(i).unwrap().buckets.iter()).sum()
};
assert_eq!(hist.total(), manual_total(&hist));
assert_eq!(hist.total(), 4);
hist.advance(1);
for v in [2, 20, 200] {
hist.record(v);
}
assert_eq!(hist.total(), manual_total(&hist));
assert_eq!(hist.total(), 7);
hist.advance(2);
hist.record(3);
assert_eq!(hist.active_slot_count(), 3);
assert_eq!(hist.total(), manual_total(&hist));
assert_eq!(hist.total(), 8);
hist.advance(3);
assert_eq!(hist.active_slot_count(), 3);
assert_eq!(hist.total(), manual_total(&hist));
assert_eq!(hist.total(), 4); }
macro_rules! test_histogram_width_edge_cases {
($name:ident, $width:expr) => {
#[test]
fn $name() {
use std::sync::LazyLock;
static SCALE: LazyLock<LogScale<{ $width }>> = LazyLock::new(LogScale::new);
assert_eq!(SCALE.num_buckets(), LogScaleConfig::<{ $width }>::BUCKETS);
let mut hist = Histogram::<(), { $width }>::with_log_scale(&SCALE, 2);
hist.record(0);
hist.record(1);
hist.record(u64::MAX);
assert_eq!(hist.total(), 3);
let p0 = hist.percentile(0.0);
assert_eq!(p0, 0);
let _ = hist.percentile(0.5);
let p100 = hist.percentile(1.0);
assert!(p100 > 0);
hist.advance(());
hist.record(1000);
assert_eq!(hist.total(), 4);
hist.advance(());
assert_eq!(hist.total(), 1);
}
};
}
#[test]
fn test_single_slot_optimization() {
let mut hist: Histogram<u64> = Histogram::new();
assert_eq!(hist.active_slot_count(), 0);
hist.record(100);
hist.record(200);
assert_eq!(hist.total(), 2);
hist.advance(1);
assert_eq!(hist.total(), 0);
assert_eq!(hist.active_slot_count(), 0);
hist.record(50);
assert_eq!(hist.total(), 1);
let p50 = hist.percentile(0.5);
assert!((48..=52).contains(&p50), "P50 = {p50}");
hist.advance(2);
assert_eq!(hist.total(), 0);
hist.record(1000);
hist.record(1000);
assert_eq!(hist.total(), 2);
}
test_histogram_width_edge_cases!(test_width_edge_1, 1);
test_histogram_width_edge_cases!(test_width_edge_2, 2);
test_histogram_width_edge_cases!(test_width_edge_3, 3);
test_histogram_width_edge_cases!(test_width_edge_4, 4);
test_histogram_width_edge_cases!(test_width_edge_5, 5);
test_histogram_width_edge_cases!(test_width_edge_6, 6);
test_histogram_width_edge_cases!(test_width_edge_7, 7);
test_histogram_width_edge_cases!(test_width_edge_8, 8);
test_histogram_width_edge_cases!(test_width_edge_9, 9);
test_histogram_width_edge_cases!(test_width_edge_10, 10);
}