use crate::common::{Mergeable, Result, Sketch, SketchError};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_insert() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 1);
assert!(!eh.is_empty());
assert!(eh.num_buckets() >= 1);
eh.insert(200, 1);
eh.insert(300, 1);
assert!(eh.num_buckets() >= 1);
}
#[test]
fn test_events_in_window() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 1);
eh.insert(200, 1);
eh.insert(300, 1);
let (estimate, lower, upper) = eh.count(500);
assert!(estimate >= 2, "Estimate {} should be >= 2", estimate);
assert!(lower <= 3, "Lower {} should be <= 3", lower);
assert!(upper >= 3, "Upper {} should be >= 3", upper);
}
#[test]
fn test_events_outside_window() {
let mut eh = ExponentialHistogram::new(100, 0.1).unwrap();
eh.insert(10, 1); eh.insert(20, 1); eh.insert(150, 1); eh.insert(180, 1);
let (estimate, _lower, _upper) = eh.count(200);
assert!(estimate >= 1, "Estimate {} should be >= 1", estimate);
assert!(estimate <= 4, "Estimate {} should be <= 4", estimate);
}
#[test]
fn test_error_bounds() {
let epsilon = 0.1;
let mut eh = ExponentialHistogram::new(10000, epsilon).unwrap();
let actual_count = 100;
for i in 0..actual_count {
eh.insert(i * 10, 1);
}
let (estimate, lower, upper) = eh.count(actual_count * 10);
assert!(
lower <= estimate,
"Lower {} should be <= estimate {}",
lower,
estimate
);
assert!(
estimate <= upper,
"Estimate {} should be <= upper {}",
estimate,
upper
);
let relative_error = (estimate as f64 - actual_count as f64).abs() / actual_count as f64;
assert!(
relative_error <= epsilon + 0.05, "Relative error {} should be <= epsilon {} + slack",
relative_error,
epsilon
);
}
#[test]
fn test_bucket_compression() {
let epsilon = 0.5; let mut eh = ExponentialHistogram::new(10000, epsilon).unwrap();
for i in 0..100 {
eh.insert(i * 10, 1);
}
let num_buckets = eh.num_buckets();
assert!(
num_buckets < 50,
"Expected compressed buckets < 50, got {}",
num_buckets
);
let expected_max = ((1.0_f64 / epsilon).ceil() as usize + 1) * 20; assert!(
num_buckets <= expected_max,
"Buckets {} exceed expected max {}",
num_buckets,
expected_max
);
}
#[test]
fn test_monotonic_timestamps() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 1);
eh.insert(200, 1);
eh.insert(150, 1);
let (estimate, _, _) = eh.count(300);
assert!(
estimate >= 2,
"Should count events even with non-monotonic insertion"
);
}
#[test]
fn test_empty_window() {
let eh = ExponentialHistogram::new(100, 0.1).unwrap();
assert!(eh.is_empty());
let (estimate, lower, upper) = eh.count(1000);
assert_eq!(estimate, 0);
assert_eq!(lower, 0);
assert_eq!(upper, 0);
}
#[test]
fn test_single_event() {
let mut eh = ExponentialHistogram::new(100, 0.1).unwrap();
eh.insert(50, 1);
let (estimate, lower, upper) = eh.count(100);
assert_eq!(estimate, 1, "Single event should be counted exactly");
assert!(lower <= 1);
assert!(upper >= 1);
let (estimate_outside, _, _) = eh.count(200);
assert!(estimate_outside <= 1, "Event should be expired or partial");
}
#[test]
fn test_merge_windows() {
let mut eh1 = ExponentialHistogram::new(1000, 0.1).unwrap();
let mut eh2 = ExponentialHistogram::new(1000, 0.1).unwrap();
eh1.insert(100, 1);
eh1.insert(200, 1);
eh2.insert(300, 1);
eh2.insert(400, 1);
eh1.merge(&eh2).unwrap();
let (estimate, _, _) = eh1.count(500);
assert!(
estimate >= 3,
"Merged histogram should have >= 3 events, got {}",
estimate
);
}
#[test]
fn test_merge_incompatible() {
let mut eh1 = ExponentialHistogram::new(1000, 0.1).unwrap();
let eh2 = ExponentialHistogram::new(500, 0.1).unwrap();
let result = eh1.merge(&eh2);
assert!(
result.is_err(),
"Should fail to merge incompatible histograms"
);
}
#[test]
fn test_invalid_parameters() {
assert!(ExponentialHistogram::new(0, 0.1).is_err());
assert!(ExponentialHistogram::new(1000, 0.0).is_err());
assert!(ExponentialHistogram::new(1000, 1.0).is_err());
assert!(ExponentialHistogram::new(1000, 1.5).is_err());
assert!(ExponentialHistogram::new(1000, -0.1).is_err());
}
#[test]
fn test_insert_multiple_count() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 5);
let (estimate, _, _) = eh.count(200);
assert!(
estimate >= 4,
"Should count approximately 5 events, got {}",
estimate
);
assert!(estimate <= 6, "Should not over-count, got {}", estimate);
}
#[test]
fn test_expire() {
let mut eh = ExponentialHistogram::new(100, 0.1).unwrap();
eh.insert(10, 1);
eh.insert(20, 1);
eh.insert(200, 1);
eh.insert(210, 1);
let buckets_before = eh.num_buckets();
eh.expire(300);
let buckets_after = eh.num_buckets();
assert!(
buckets_after <= buckets_before,
"Expire should not increase buckets"
);
}
#[test]
fn test_serialization() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 1);
eh.insert(200, 2);
eh.insert(300, 3);
let bytes = eh.serialize();
let restored = ExponentialHistogram::deserialize(&bytes).unwrap();
assert_eq!(eh.window_size(), restored.window_size());
assert_eq!(eh.epsilon(), restored.epsilon());
assert_eq!(eh.num_buckets(), restored.num_buckets());
let (est1, _, _) = eh.count(400);
let (est2, _, _) = restored.count(400);
assert_eq!(est1, est2);
}
#[test]
fn test_sketch_trait() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.update(&(100u64, 1u64));
eh.update(&(200u64, 1u64));
let estimate = eh.estimate();
assert!(
estimate >= 1.0,
"Estimate should be >= 1.0, got {}",
estimate
);
}
#[test]
fn test_large_count_decomposition() {
let mut eh = ExponentialHistogram::new(10000, 0.1).unwrap();
eh.insert(100, 15);
let (estimate, _, _) = eh.count(200);
assert!(estimate >= 14, "Should count ~15 events, got {}", estimate);
assert!(estimate <= 16, "Should not over-count, got {}", estimate);
}
#[test]
fn test_bounds_correctness() {
let epsilon = 0.2;
let mut eh = ExponentialHistogram::new(1000, epsilon).unwrap();
for i in 0..50 {
eh.insert(i * 10, 1);
}
let (estimate, lower, upper) = eh.count(600);
assert!(lower <= estimate);
assert!(estimate <= upper);
assert!(lower >= 30, "Lower bound {} too low", lower);
assert!(upper <= 70, "Upper bound {} too high", upper);
}
#[test]
fn test_memory_usage() {
let eh = ExponentialHistogram::new(1000, 0.1).unwrap();
let base_memory = eh.memory_usage();
assert!(base_memory > 0);
let mut eh2 = ExponentialHistogram::new(1000, 0.1).unwrap();
for i in 0..100 {
eh2.insert(i * 10, 1);
}
let with_data_memory = eh2.memory_usage();
assert!(with_data_memory > base_memory);
}
#[test]
fn test_clone() {
let mut eh = ExponentialHistogram::new(1000, 0.1).unwrap();
eh.insert(100, 5);
let cloned = eh.clone();
let (est1, _, _) = eh.count(200);
let (est2, _, _) = cloned.count(200);
assert_eq!(est1, est2);
}
#[test]
fn test_stress() {
let mut eh = ExponentialHistogram::new(100000, 0.05).unwrap();
for i in 0..10000 {
eh.insert(i, 1);
}
let (estimate, lower, upper) = eh.count(15000);
assert!(estimate >= 9000, "Estimate {} too low", estimate);
assert!(lower <= estimate);
assert!(estimate <= upper);
let epsilon_val: f64 = 0.05;
let k = (1.0_f64 / epsilon_val).ceil() as usize;
let max_buckets = (k + 1) * 64; assert!(
eh.num_buckets() <= max_buckets,
"Too many buckets: {} > {}",
eh.num_buckets(),
max_buckets
);
}
}
#[derive(Clone, Debug)]
struct EHBucket {
timestamp: u64,
count: u64,
}
#[derive(Clone, Debug)]
pub struct ExponentialHistogram {
buckets: Vec<EHBucket>,
window_size: u64,
epsilon: f64,
k: usize,
last_timestamp: u64,
}
impl ExponentialHistogram {
pub fn new(window_size: u64, epsilon: f64) -> Result<Self> {
if window_size == 0 {
return Err(SketchError::InvalidParameter {
param: "window_size".to_string(),
value: "0".to_string(),
constraint: "must be > 0".to_string(),
});
}
if epsilon <= 0.0 || epsilon >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "epsilon".to_string(),
value: epsilon.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let k = (1.0_f64 / epsilon).ceil() as usize;
Ok(ExponentialHistogram {
buckets: Vec::new(),
window_size,
epsilon,
k,
last_timestamp: 0,
})
}
#[inline]
pub fn window_size(&self) -> u64 {
self.window_size
}
#[inline]
pub fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline]
pub fn k(&self) -> usize {
self.k
}
#[inline]
pub fn num_buckets(&self) -> usize {
self.buckets.len()
}
pub fn insert(&mut self, timestamp: u64, count: u64) {
if count == 0 {
return;
}
self.last_timestamp = self.last_timestamp.max(timestamp);
let mut remaining = count;
while remaining > 0 {
let power = 63 - remaining.leading_zeros();
let bucket_count = 1u64 << power;
self.buckets.insert(
0,
EHBucket {
timestamp,
count: bucket_count,
},
);
remaining -= bucket_count;
}
self.compress();
}
fn compress(&mut self) {
if self.buckets.len() < 2 {
return;
}
let mut changed = true;
while changed {
changed = false;
let mut i = 0;
while i < self.buckets.len() {
let current_count = self.buckets[i].count;
let same_count_indices: Vec<usize> = (0..self.buckets.len())
.filter(|&j| self.buckets[j].count == current_count)
.collect();
if same_count_indices.len() > self.k + 1 {
let mut indices_by_time: Vec<(usize, u64)> = same_count_indices
.iter()
.map(|&idx| (idx, self.buckets[idx].timestamp))
.collect();
indices_by_time.sort_by_key(|&(_, ts)| ts);
let oldest_idx = indices_by_time[0].0;
let second_oldest_idx = indices_by_time[1].0;
let older_timestamp = self.buckets[oldest_idx]
.timestamp
.min(self.buckets[second_oldest_idx].timestamp);
let merged_count = current_count * 2;
let (keep_idx, remove_idx) = if oldest_idx < second_oldest_idx {
(oldest_idx, second_oldest_idx)
} else {
(second_oldest_idx, oldest_idx)
};
self.buckets[keep_idx] = EHBucket {
timestamp: older_timestamp,
count: merged_count,
};
self.buckets.remove(remove_idx);
changed = true;
break; }
i += 1;
while i < self.buckets.len() && self.buckets[i].count == current_count {
i += 1;
}
}
}
}
pub fn count(&self, current_time: u64) -> (u64, u64, u64) {
if self.buckets.is_empty() {
return (0, 0, 0);
}
let window_start = current_time.saturating_sub(self.window_size);
let mut total = 0u64;
let mut oldest_partial_count = 0u64;
for bucket in &self.buckets {
if bucket.timestamp > current_time {
continue;
}
if bucket.timestamp >= window_start {
total += bucket.count;
} else {
oldest_partial_count = bucket.count;
break;
}
}
let estimate = total + oldest_partial_count / 2;
let lower = total;
let upper = total + oldest_partial_count;
(estimate, lower, upper)
}
pub fn expire(&mut self, current_time: u64) {
let window_start = current_time.saturating_sub(self.window_size);
let mut found_outside = false;
self.buckets.retain(|bucket| {
if bucket.timestamp >= window_start {
true } else if !found_outside {
found_outside = true;
true } else {
false }
});
}
pub fn clear(&mut self) {
self.buckets.clear();
self.last_timestamp = 0;
}
#[inline]
pub fn error_bound(&self) -> f64 {
self.epsilon
}
pub fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>() + self.buckets.len() * std::mem::size_of::<EHBucket>()
}
}
impl Sketch for ExponentialHistogram {
type Item = (u64, u64);
fn update(&mut self, item: &Self::Item) {
self.insert(item.0, item.1);
}
fn estimate(&self) -> f64 {
let (estimate, _, _) = self.count(self.last_timestamp);
estimate as f64
}
fn is_empty(&self) -> bool {
self.buckets.is_empty()
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.window_size.to_le_bytes());
bytes.extend_from_slice(&self.epsilon.to_le_bytes());
bytes.extend_from_slice(&(self.k as u64).to_le_bytes());
bytes.extend_from_slice(&self.last_timestamp.to_le_bytes());
bytes.extend_from_slice(&(self.buckets.len() as u64).to_le_bytes());
for bucket in &self.buckets {
bytes.extend_from_slice(&bucket.timestamp.to_le_bytes());
bytes.extend_from_slice(&bucket.count.to_le_bytes());
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self> {
const HEADER_SIZE: usize = 40;
if bytes.len() < HEADER_SIZE {
return Err(SketchError::DeserializationError(
"Insufficient data for ExponentialHistogram header".to_string(),
));
}
let window_size = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
let epsilon = f64::from_le_bytes(bytes[8..16].try_into().unwrap());
let k = u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize;
let last_timestamp = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
let num_buckets = u64::from_le_bytes(bytes[32..40].try_into().unwrap()) as usize;
let expected_len = HEADER_SIZE + num_buckets * 16;
if bytes.len() < expected_len {
return Err(SketchError::DeserializationError(format!(
"Expected {} bytes, got {}",
expected_len,
bytes.len()
)));
}
let mut buckets = Vec::with_capacity(num_buckets);
let mut offset = HEADER_SIZE;
for _ in 0..num_buckets {
let timestamp = u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap());
let count = u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap());
buckets.push(EHBucket { timestamp, count });
offset += 16;
}
Ok(ExponentialHistogram {
buckets,
window_size,
epsilon,
k,
last_timestamp,
})
}
}
impl Mergeable for ExponentialHistogram {
fn merge(&mut self, other: &Self) -> Result<()> {
if self.window_size != other.window_size {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Window size mismatch: {} vs {}",
self.window_size, other.window_size
),
});
}
if (self.epsilon - other.epsilon).abs() > 1e-10 {
return Err(SketchError::IncompatibleSketches {
reason: format!("Epsilon mismatch: {} vs {}", self.epsilon, other.epsilon),
});
}
for bucket in &other.buckets {
self.buckets.push(bucket.clone());
}
self.last_timestamp = self.last_timestamp.max(other.last_timestamp);
self.buckets.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
self.compress();
Ok(())
}
}