use crate::common::{Mergeable, Sketch, SketchError};
use std::cmp::Ordering;
use twox_hash::XxHash64;
#[derive(Clone, Debug, Copy)]
struct ElasticBucket {
item_hash: u64,
frequency: u64,
elastic_counter: u64,
is_occupied: bool,
}
impl ElasticBucket {
fn empty() -> Self {
ElasticBucket {
item_hash: 0,
frequency: 0,
elastic_counter: 0,
is_occupied: false,
}
}
fn new(item_hash: u64, frequency: u64) -> Self {
ElasticBucket {
item_hash,
frequency,
elastic_counter: 0,
is_occupied: true,
}
}
}
#[derive(Clone, Debug)]
pub struct ElasticSketch {
buckets: Vec<ElasticBucket>,
bucket_count: usize,
mask: usize,
depth: usize,
elastic_ratio: f64,
total_count: u64,
}
impl ElasticSketch {
pub fn new(bucket_count: usize, depth: usize) -> Result<Self, SketchError> {
Self::with_elastic_ratio(bucket_count, depth, 0.2)
}
pub fn with_elastic_ratio(
bucket_count: usize,
depth: usize,
elastic_ratio: f64,
) -> Result<Self, SketchError> {
if bucket_count == 0 {
return Err(SketchError::InvalidParameter {
param: "bucket_count".to_string(),
value: "0".to_string(),
constraint: "must be > 0".to_string(),
});
}
if depth == 0 || depth > 8 {
return Err(SketchError::InvalidParameter {
param: "depth".to_string(),
value: depth.to_string(),
constraint: "must be in [1, 8]".to_string(),
});
}
if !(0.0..=1.0).contains(&elastic_ratio) {
return Err(SketchError::InvalidParameter {
param: "elastic_ratio".to_string(),
value: elastic_ratio.to_string(),
constraint: "must be in [0.0, 1.0]".to_string(),
});
}
let bucket_count = bucket_count.next_power_of_two();
let mask = bucket_count - 1;
let buckets = vec![ElasticBucket::empty(); depth * bucket_count];
Ok(ElasticSketch {
buckets,
bucket_count,
mask,
depth,
elastic_ratio,
total_count: 0,
})
}
#[inline]
fn hash_item(item: &[u8]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
hasher.finish()
}
#[inline]
fn bucket_index(&self, row: usize, item_hash: u64) -> usize {
let col = (item_hash as usize) & self.mask;
row * self.bucket_count + col
}
pub fn update(&mut self, item: &[u8], count: u64) {
let item_hash = Self::hash_item(item);
self.total_count = self.total_count.saturating_add(count);
let mut min_frequency = u64::MAX;
let mut min_row = 0;
let mut found_empty = false;
for row in 0..self.depth {
let idx = self.bucket_index(row, item_hash);
let bucket = self.buckets[idx];
if !bucket.is_occupied {
min_row = row;
found_empty = true;
break;
} else if bucket.item_hash == item_hash {
self.buckets[idx].frequency = self.buckets[idx].frequency.saturating_add(count);
self.buckets[idx].elastic_counter =
self.buckets[idx].elastic_counter.saturating_add(count);
return;
} else if bucket.frequency < min_frequency {
min_frequency = bucket.frequency;
min_row = row;
}
}
if !found_empty {
}
let idx = self.bucket_index(min_row, item_hash);
if !self.buckets[idx].is_occupied {
self.buckets[idx] = ElasticBucket::new(item_hash, count);
} else {
self.buckets[idx].elastic_counter =
self.buckets[idx].elastic_counter.saturating_add(count);
}
}
pub fn estimate(&self, item: &[u8]) -> u64 {
let item_hash = Self::hash_item(item);
let mut max_estimate = 0u64;
let mut found_count = 0;
for row in 0..self.depth {
let idx = self.bucket_index(row, item_hash);
let bucket = self.buckets[idx];
if bucket.is_occupied && bucket.item_hash == item_hash {
let elastic_estimate = bucket.frequency
+ (bucket.elastic_counter as f64 * self.elastic_ratio).floor() as u64;
max_estimate = max_estimate.max(elastic_estimate);
found_count += 1;
}
}
if found_count > 0 {
max_estimate
} else {
0
}
}
pub fn heavy_hitters(&self, threshold: u64) -> Vec<(u64, u64)> {
use std::collections::BTreeMap;
let mut items: BTreeMap<u64, u64> = BTreeMap::new();
for bucket in &self.buckets {
if bucket.is_occupied {
let elastic_estimate = bucket.frequency
+ (bucket.elastic_counter as f64 * self.elastic_ratio).floor() as u64;
if elastic_estimate >= threshold {
items
.entry(bucket.item_hash)
.and_modify(|f| *f = (*f).max(elastic_estimate))
.or_insert(elastic_estimate);
}
}
}
let mut result: Vec<(u64, u64)> = items.into_iter().collect();
result.sort_by(|a, b| {
match b.1.cmp(&a.1) {
Ordering::Equal => a.0.cmp(&b.0),
other => other,
}
});
result
}
pub fn merge(&mut self, other: &ElasticSketch) -> Result<(), SketchError> {
if self.bucket_count != other.bucket_count {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"bucket_count mismatch: {} vs {}",
self.bucket_count, other.bucket_count
),
});
}
if self.depth != other.depth {
return Err(SketchError::IncompatibleSketches {
reason: format!("depth mismatch: {} vs {}", self.depth, other.depth),
});
}
if (self.elastic_ratio - other.elastic_ratio).abs() > 1e-10 {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"elastic_ratio mismatch: {} vs {}",
self.elastic_ratio, other.elastic_ratio
),
});
}
for i in 0..self.buckets.len() {
let self_bucket = self.buckets[i];
let other_bucket = other.buckets[i];
match (self_bucket.is_occupied, other_bucket.is_occupied) {
(false, true) => {
self.buckets[i] = other_bucket;
}
(true, true) => {
if self_bucket.item_hash == other_bucket.item_hash {
self.buckets[i].frequency = self.buckets[i]
.frequency
.saturating_add(other_bucket.frequency);
self.buckets[i].elastic_counter = self.buckets[i]
.elastic_counter
.saturating_add(other_bucket.elastic_counter);
} else if other_bucket.frequency > self_bucket.frequency {
self.buckets[i] = other_bucket;
}
}
_ => {
}
}
}
self.total_count = self.total_count.saturating_add(other.total_count);
Ok(())
}
pub fn reset(&mut self) {
self.buckets
.iter_mut()
.for_each(|b| *b = ElasticBucket::empty());
self.total_count = 0;
}
#[inline]
pub fn bucket_count(&self) -> usize {
self.bucket_count
}
#[inline]
pub fn depth(&self) -> usize {
self.depth
}
#[inline]
pub fn elastic_ratio(&self) -> f64 {
self.elastic_ratio
}
#[inline]
pub fn total_count(&self) -> u64 {
self.total_count
}
#[inline]
pub fn memory_usage(&self) -> usize {
self.buckets.len() * std::mem::size_of::<ElasticBucket>()
}
}
impl Sketch for ElasticSketch {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
ElasticSketch::update(self, &item.to_le_bytes(), 1);
}
fn estimate(&self) -> f64 {
self.total_count as f64
}
fn is_empty(&self) -> bool {
self.buckets.iter().all(|b| !b.is_occupied)
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(self.bucket_count as u64).to_le_bytes());
bytes.extend_from_slice(&(self.depth as u64).to_le_bytes());
bytes.extend_from_slice(&self.elastic_ratio.to_le_bytes());
bytes.extend_from_slice(&self.total_count.to_le_bytes());
for bucket in &self.buckets {
bytes.extend_from_slice(&bucket.item_hash.to_le_bytes());
bytes.extend_from_slice(&bucket.frequency.to_le_bytes());
bytes.extend_from_slice(&bucket.elastic_counter.to_le_bytes());
bytes.push(if bucket.is_occupied { 1 } else { 0 });
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 32 {
return Err(SketchError::DeserializationError(
"insufficient bytes for header".to_string(),
));
}
let mut offset = 0;
let bucket_count =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid bucket_count".to_string())
})?) as usize;
offset += 8;
let depth = u64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid depth".to_string()))?,
) as usize;
offset += 8;
let elastic_ratio =
f64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid elastic_ratio".to_string())
})?);
offset += 8;
let total_count =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid total_count".to_string())
})?);
offset += 8;
let bucket_size = 8 + 8 + 8 + 1; let expected_bytes = offset + bucket_size * bucket_count * depth;
if bytes.len() < expected_bytes {
return Err(SketchError::DeserializationError(
"insufficient bytes for buckets".to_string(),
));
}
let mut buckets = Vec::with_capacity(bucket_count * depth);
for _ in 0..(bucket_count * depth) {
let item_hash =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid bucket".to_string())
})?);
offset += 8;
let frequency =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid bucket".to_string())
})?);
offset += 8;
let elastic_counter =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid bucket".to_string())
})?);
offset += 8;
let is_occupied = bytes[offset] != 0;
offset += 1;
buckets.push(ElasticBucket {
item_hash,
frequency,
elastic_counter,
is_occupied,
});
}
let mask = bucket_count - 1;
Ok(ElasticSketch {
buckets,
bucket_count,
mask,
depth,
elastic_ratio,
total_count,
})
}
}
impl Mergeable for ElasticSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
ElasticSketch::merge(self, other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_construction() {
let sketch = ElasticSketch::new(512, 3).unwrap();
assert_eq!(sketch.bucket_count(), 512);
assert_eq!(sketch.depth(), 3);
assert!((sketch.elastic_ratio() - 0.2).abs() < 1e-10);
assert!(sketch.is_empty());
}
#[test]
fn test_with_elastic_ratio() {
let sketch = ElasticSketch::with_elastic_ratio(512, 3, 0.5).unwrap();
assert!((sketch.elastic_ratio() - 0.5).abs() < 1e-10);
}
#[test]
fn test_invalid_parameters() {
assert!(ElasticSketch::new(0, 3).is_err());
assert!(ElasticSketch::new(512, 0).is_err());
assert!(ElasticSketch::new(512, 9).is_err());
assert!(ElasticSketch::with_elastic_ratio(512, 3, -0.1).is_err());
assert!(ElasticSketch::with_elastic_ratio(512, 3, 1.1).is_err());
}
#[test]
fn test_single_item_insertion() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"flow1", 1);
assert!(!sketch.is_empty());
assert_eq!(sketch.estimate(b"flow1"), 1);
}
#[test]
fn test_multiple_updates() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"item", 1);
sketch.update(b"item", 2);
sketch.update(b"item", 3);
let estimate = sketch.estimate(b"item");
assert!(
(6..=8).contains(&estimate),
"Expected estimate near 6, got {}",
estimate
);
}
#[test]
fn test_multiple_items_different_frequencies() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"heavy", 10);
sketch.update(b"medium", 5);
sketch.update(b"light", 1);
assert_eq!(sketch.estimate(b"heavy"), 10);
assert_eq!(sketch.estimate(b"medium"), 5);
assert_eq!(sketch.estimate(b"light"), 1);
}
#[test]
fn test_nonexistent_item() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"item1", 1);
assert_eq!(sketch.estimate(b"nonexistent"), 0);
}
#[test]
fn test_heavy_hitters() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"heavy1", 100);
sketch.update(b"heavy2", 50);
sketch.update(b"medium", 10);
sketch.update(b"light", 1);
let hitters = sketch.heavy_hitters(20);
assert!(hitters.len() >= 2);
assert!(hitters[0].1 >= 50);
}
#[test]
fn test_merge_sketches() {
let mut sketch1 = ElasticSketch::new(512, 3).unwrap();
let mut sketch2 = ElasticSketch::new(512, 3).unwrap();
sketch1.update(b"item1", 5);
sketch2.update(b"item1", 3);
sketch2.update(b"item2", 2);
sketch1.merge(&sketch2).unwrap();
assert_eq!(sketch1.estimate(b"item1"), 8);
assert_eq!(sketch1.estimate(b"item2"), 2);
}
#[test]
fn test_merge_incompatible_sketches() {
let sketch1 = ElasticSketch::new(512, 3).unwrap();
let sketch2 = ElasticSketch::new(256, 3).unwrap();
let mut s1 = sketch1.clone();
assert!(s1.merge(&sketch2).is_err());
let sketch3 = ElasticSketch::new(512, 4).unwrap(); let mut s1 = sketch1.clone();
assert!(s1.merge(&sketch3).is_err());
}
#[test]
fn test_reset() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"item1", 5);
sketch.update(b"item2", 3);
assert!(!sketch.is_empty());
assert_eq!(sketch.total_count(), 8);
sketch.reset();
assert!(sketch.is_empty());
assert_eq!(sketch.total_count(), 0);
assert_eq!(sketch.estimate(b"item1"), 0);
}
#[test]
fn test_serialization() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"item1", 5);
sketch.update(b"item2", 3);
let bytes = sketch.serialize();
let deserialized = ElasticSketch::deserialize(&bytes).unwrap();
assert_eq!(deserialized.bucket_count(), 512);
assert_eq!(deserialized.depth(), 3);
assert_eq!(deserialized.estimate(b"item1"), 5);
assert_eq!(deserialized.estimate(b"item2"), 3);
}
#[test]
fn test_stability() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
sketch.update(b"stable_item", 10);
let estimate1 = sketch.estimate(b"stable_item");
let estimate2 = sketch.estimate(b"stable_item");
let estimate3 = sketch.estimate(b"stable_item");
assert_eq!(estimate1, estimate2);
assert_eq!(estimate2, estimate3);
assert_eq!(estimate1, 10);
}
#[test]
fn test_memory_usage() {
let sketch = ElasticSketch::new(512, 3).unwrap();
let memory = sketch.memory_usage();
let expected_buckets = 512 * 3;
let bucket_size = std::mem::size_of::<ElasticBucket>();
let expected_memory = expected_buckets * bucket_size;
assert_eq!(memory, expected_memory);
}
#[test]
fn test_zipfian_distribution() {
let mut sketch = ElasticSketch::new(1024, 4).unwrap();
for i in 1..=20 {
let item = format!("item{}", i).into_bytes();
let frequency = 100 / i as u64;
sketch.update(&item, frequency);
}
let hitters = sketch.heavy_hitters(10);
assert!(!hitters.is_empty());
assert_eq!(sketch.estimate(b"item1"), 100);
}
#[test]
fn test_large_frequencies() {
let mut sketch = ElasticSketch::new(512, 3).unwrap();
let large_count = 1_000_000u64;
sketch.update(b"large_item", large_count);
assert_eq!(sketch.estimate(b"large_item"), large_count);
}
#[test]
fn test_bucket_rounding() {
let sketch1 = ElasticSketch::new(500, 3).unwrap();
assert_eq!(sketch1.bucket_count(), 512);
let sketch2 = ElasticSketch::new(1024, 3).unwrap();
assert_eq!(sketch2.bucket_count(), 1024);
let sketch3 = ElasticSketch::new(1000, 3).unwrap();
assert_eq!(sketch3.bucket_count(), 1024); }
}