#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ExponentialHistogram {
scale: i32,
base: f64,
log_base: f64,
positive_buckets: Vec<u64>,
negative_buckets: Vec<u64>,
positive_offset: i32,
negative_offset: i32,
zero_count: u64,
zero_threshold: f64,
count: u64,
sum: f64,
min: f64,
max: f64,
}
impl ExponentialHistogram {
pub fn new(scale: i32) -> Self {
let base = 2.0_f64.powf(2.0_f64.powi(-scale));
Self {
scale,
base,
log_base: base.ln(),
positive_buckets: Vec::new(),
negative_buckets: Vec::new(),
positive_offset: 0,
negative_offset: 0,
zero_count: 0,
zero_threshold: 0.0,
count: 0,
sum: 0.0,
min: f64::MAX,
max: f64::MIN,
}
}
pub fn default_scale() -> Self {
Self::new(3)
}
#[inline]
fn bucket_index(&self, value: f64) -> i32 {
debug_assert!(value > 0.0);
(value.log2() * (1 << self.scale) as f64).floor() as i32
}
#[inline]
fn bucket_lower_bound(&self, index: i32) -> f64 {
self.base.powi(index)
}
pub fn record(&mut self, value: f64) {
self.count += 1;
self.sum += value;
self.min = self.min.min(value);
self.max = self.max.max(value);
if value.abs() <= self.zero_threshold {
self.zero_count += 1;
} else if value > 0.0 {
let idx = self.bucket_index(value);
self.increment_positive(idx);
} else {
let idx = self.bucket_index(-value);
self.increment_negative(idx);
}
}
fn increment_positive(&mut self, idx: i32) {
if self.positive_buckets.is_empty() {
self.positive_offset = idx;
self.positive_buckets.push(1);
} else {
let relative_idx = idx - self.positive_offset;
if relative_idx < 0 {
let prepend_count = (-relative_idx) as usize;
let mut new_buckets = vec![0; prepend_count];
new_buckets.append(&mut self.positive_buckets);
self.positive_buckets = new_buckets;
self.positive_offset = idx;
self.positive_buckets[0] = 1;
} else if relative_idx as usize >= self.positive_buckets.len() {
self.positive_buckets.resize(relative_idx as usize + 1, 0);
self.positive_buckets[relative_idx as usize] = 1;
} else {
self.positive_buckets[relative_idx as usize] += 1;
}
}
}
fn increment_negative(&mut self, idx: i32) {
if self.negative_buckets.is_empty() {
self.negative_offset = idx;
self.negative_buckets.push(1);
} else {
let relative_idx = idx - self.negative_offset;
if relative_idx < 0 {
let prepend_count = (-relative_idx) as usize;
let mut new_buckets = vec![0; prepend_count];
new_buckets.append(&mut self.negative_buckets);
self.negative_buckets = new_buckets;
self.negative_offset = idx;
self.negative_buckets[0] = 1;
} else if relative_idx as usize >= self.negative_buckets.len() {
self.negative_buckets.resize(relative_idx as usize + 1, 0);
self.negative_buckets[relative_idx as usize] = 1;
} else {
self.negative_buckets[relative_idx as usize] += 1;
}
}
}
pub fn merge(&mut self, other: &ExponentialHistogram) {
if other.count == 0 {
return;
}
assert_eq!(self.scale, other.scale, "Scale mismatch in merge");
for (i, &count) in other.positive_buckets.iter().enumerate() {
if count > 0 {
let idx = other.positive_offset + i as i32;
self.add_positive_count(idx, count);
}
}
for (i, &count) in other.negative_buckets.iter().enumerate() {
if count > 0 {
let idx = other.negative_offset + i as i32;
self.add_negative_count(idx, count);
}
}
self.zero_count += other.zero_count;
self.count += other.count;
self.sum += other.sum;
if other.count > 0 {
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
}
}
fn add_positive_count(&mut self, idx: i32, count: u64) {
if self.positive_buckets.is_empty() {
self.positive_offset = idx;
self.positive_buckets.push(count);
} else {
let relative_idx = idx - self.positive_offset;
if relative_idx < 0 {
let prepend_count = (-relative_idx) as usize;
let mut new_buckets = vec![0; prepend_count];
new_buckets.append(&mut self.positive_buckets);
self.positive_buckets = new_buckets;
self.positive_offset = idx;
self.positive_buckets[0] += count;
} else if relative_idx as usize >= self.positive_buckets.len() {
self.positive_buckets.resize(relative_idx as usize + 1, 0);
self.positive_buckets[relative_idx as usize] += count;
} else {
self.positive_buckets[relative_idx as usize] += count;
}
}
}
fn add_negative_count(&mut self, idx: i32, count: u64) {
if self.negative_buckets.is_empty() {
self.negative_offset = idx;
self.negative_buckets.push(count);
} else {
let relative_idx = idx - self.negative_offset;
if relative_idx < 0 {
let prepend_count = (-relative_idx) as usize;
let mut new_buckets = vec![0; prepend_count];
new_buckets.append(&mut self.negative_buckets);
self.negative_buckets = new_buckets;
self.negative_offset = idx;
self.negative_buckets[0] += count;
} else if relative_idx as usize >= self.negative_buckets.len() {
self.negative_buckets.resize(relative_idx as usize + 1, 0);
self.negative_buckets[relative_idx as usize] += count;
} else {
self.negative_buckets[relative_idx as usize] += count;
}
}
}
pub fn count(&self) -> u64 {
self.count
}
pub fn sum(&self) -> f64 {
self.sum
}
pub fn mean(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.sum / self.count as f64
}
}
pub fn min(&self) -> f64 {
if self.count > 0 { self.min } else { 0.0 }
}
pub fn max(&self) -> f64 {
if self.count > 0 { self.max } else { 0.0 }
}
pub fn quantile(&self, q: f64) -> f64 {
if self.count == 0 {
return 0.0;
}
let q = q.clamp(0.0, 1.0);
let target_rank = (q * self.count as f64).ceil() as u64;
let mut cumulative = 0u64;
for (i, &count) in self.negative_buckets.iter().enumerate().rev() {
if count > 0 {
cumulative += count;
if cumulative >= target_rank {
let idx = self.negative_offset + i as i32;
return -self.bucket_lower_bound(idx);
}
}
}
cumulative += self.zero_count;
if cumulative >= target_rank {
return 0.0;
}
for (i, &count) in self.positive_buckets.iter().enumerate() {
if count > 0 {
cumulative += count;
if cumulative >= target_rank {
let idx = self.positive_offset + i as i32;
let lower = self.bucket_lower_bound(idx);
let upper = self.bucket_lower_bound(idx + 1);
return (lower * upper).sqrt();
}
}
}
self.max
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn clear(&mut self) {
self.positive_buckets.clear();
self.negative_buckets.clear();
self.positive_offset = 0;
self.negative_offset = 0;
self.zero_count = 0;
self.count = 0;
self.sum = 0.0;
self.min = f64::MAX;
self.max = f64::MIN;
}
pub fn bucket_count(&self) -> usize {
self.positive_buckets.len() + self.negative_buckets.len()
}
pub fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ self.positive_buckets.len() * std::mem::size_of::<u64>()
+ self.negative_buckets.len() * std::mem::size_of::<u64>()
}
}
impl Default for ExponentialHistogram {
fn default() -> Self {
Self::default_scale()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_recording() {
let mut hist = ExponentialHistogram::new(3);
for i in 1..=100 {
hist.record(i as f64);
}
assert_eq!(hist.count(), 100);
assert!((hist.mean() - 50.5).abs() < 0.01);
assert_eq!(hist.min(), 1.0);
assert_eq!(hist.max(), 100.0);
}
#[test]
fn test_merge() {
let mut hist1 = ExponentialHistogram::new(3);
let mut hist2 = ExponentialHistogram::new(3);
for i in 1..=50 {
hist1.record(i as f64);
}
for i in 51..=100 {
hist2.record(i as f64);
}
hist1.merge(&hist2);
assert_eq!(hist1.count(), 100);
assert!((hist1.mean() - 50.5).abs() < 0.01);
}
#[test]
fn test_quantiles() {
let mut hist = ExponentialHistogram::new(3);
for i in 1..=100 {
hist.record(i as f64);
}
let p50 = hist.quantile(0.50);
let p99 = hist.quantile(0.99);
assert!(p50 > 30.0 && p50 < 70.0, "P50 was {}", p50);
assert!(p99 > 80.0, "P99 was {}", p99);
}
#[test]
fn test_latency_distribution() {
let mut hist = ExponentialHistogram::new(3);
for _ in 0..900 {
hist.record(5.0); }
for _ in 0..90 {
hist.record(50.0); }
for _ in 0..10 {
hist.record(500.0); }
assert_eq!(hist.count(), 1000);
let p50 = hist.quantile(0.50);
let p99 = hist.quantile(0.99);
assert!(p50 < 20.0, "P50 was {}", p50);
assert!(p99 > 5.0, "P99 was {}", p99);
}
}