use crate::common::SketchError;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
#[derive(Clone)]
pub struct HeavyKeeper {
k: usize,
depth: usize,
width: usize,
buckets: Vec<Vec<u32>>,
decay_factor: f64,
heap: BinaryHeap<Reverse<HeapEntry>>,
total_updates: u64,
#[allow(dead_code)]
epsilon: f64,
#[allow(dead_code)]
delta: f64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct HeapEntry {
count: u32,
item_hash: u64,
}
impl Ord for HeapEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.count
.cmp(&other.count)
.then_with(|| self.item_hash.cmp(&other.item_hash))
}
}
impl PartialOrd for HeapEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl HeavyKeeper {
pub fn new(k: usize, epsilon: f64, delta: f64) -> Result<Self, SketchError> {
if k == 0 {
return Err(SketchError::InvalidParameter {
param: "k".to_string(),
value: k.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(),
});
}
if delta <= 0.0 || delta >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "delta".to_string(),
value: delta.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let depth = ((1.0 / delta).ln()).ceil() as usize;
let depth = depth.max(1);
let width = (std::f64::consts::E / epsilon).ceil() as usize;
let width = width.max(16);
let buckets = vec![vec![0u32; width]; depth];
Ok(Self {
k,
depth,
width,
buckets,
decay_factor: 1.08, heap: BinaryHeap::new(),
total_updates: 0,
epsilon,
delta,
})
}
pub fn update(&mut self, item: &[u8]) {
self.total_updates += 1;
let item_hash = Self::hash_item(item);
let mut min_count = u32::MAX;
let mut positions = Vec::with_capacity(self.depth);
for i in 0..self.depth {
let pos = Self::hash_position(item_hash, i, self.width);
positions.push((i, pos));
min_count = min_count.min(self.buckets[i][pos]);
}
let new_count = min_count.saturating_add(1);
for (row, col) in positions {
if self.buckets[row][col] < new_count {
self.buckets[row][col] = new_count;
}
}
self.update_heap(item_hash, new_count);
}
pub fn estimate(&self, item: &[u8]) -> u32 {
let item_hash = Self::hash_item(item);
let mut min_count = u32::MAX;
for i in 0..self.depth {
let pos = Self::hash_position(item_hash, i, self.width);
min_count = min_count.min(self.buckets[i][pos]);
}
min_count
}
pub fn top_k(&self) -> Vec<(u64, u32)> {
let mut entries: Vec<_> = self
.heap
.iter()
.map(|Reverse(entry)| (entry.item_hash, entry.count))
.collect();
entries.sort_by(|a, b| b.1.cmp(&a.1));
entries
}
pub fn decay(&mut self) {
for row in &mut self.buckets {
for count in row.iter_mut() {
if *count > 0 {
*count = ((*count as f64) / self.decay_factor) as u32;
}
}
}
self.rebuild_heap();
}
pub fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.depth != other.depth || self.width != other.width {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"dimension mismatch: {}×{} vs {}×{} (different epsilon/delta parameters)",
self.depth, self.width, other.depth, other.width
),
});
}
if self.k != other.k {
return Err(SketchError::IncompatibleSketches {
reason: format!("k mismatch: {} vs {}", self.k, other.k),
});
}
for i in 0..self.depth {
for j in 0..self.width {
self.buckets[i][j] = self.buckets[i][j].saturating_add(other.buckets[i][j]);
}
}
self.total_updates = self.total_updates.saturating_add(other.total_updates);
self.rebuild_heap();
Ok(())
}
pub fn stats(&self) -> HeavyKeeperStats {
let array_memory = (self.depth * self.width * 32) as u64;
let heap_memory = (self.k * 96) as u64;
HeavyKeeperStats {
total_updates: self.total_updates,
k: self.k,
memory_bits: array_memory + heap_memory,
depth: self.depth,
width: self.width,
}
}
fn update_heap(&mut self, item_hash: u64, count: u32) {
let existing = self
.heap
.iter()
.any(|Reverse(entry)| entry.item_hash == item_hash);
if existing {
self.heap
.retain(|Reverse(entry)| entry.item_hash != item_hash);
self.heap.push(Reverse(HeapEntry { count, item_hash }));
} else {
if self.heap.len() < self.k {
self.heap.push(Reverse(HeapEntry { count, item_hash }));
} else {
if let Some(Reverse(min_entry)) = self.heap.peek() {
if count > min_entry.count {
self.heap.pop();
self.heap.push(Reverse(HeapEntry { count, item_hash }));
}
}
}
}
}
fn rebuild_heap(&mut self) {
self.heap.clear();
let mut candidates: Vec<(u32, u64)> = Vec::new();
for i in 0..self.depth {
for j in 0..self.width {
let count = self.buckets[i][j];
if count > 0 {
let synthetic_hash = ((i as u64) << 32) | (j as u64);
candidates.push((count, synthetic_hash));
}
}
}
candidates.sort_by(|a, b| b.0.cmp(&a.0));
candidates.truncate(self.k);
for (count, hash) in candidates {
self.heap.push(Reverse(HeapEntry {
count,
item_hash: hash,
}));
}
}
#[inline]
fn hash_item(item: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 14695981039346656037;
const FNV_PRIME: u64 = 1099511628211;
let mut hash = FNV_OFFSET;
for &byte in item {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[inline]
fn hash_position(item_hash: u64, row: usize, width: usize) -> usize {
let h1 = item_hash as usize;
let h2 = (item_hash >> 32) as usize;
(h1.wrapping_add(row.wrapping_mul(h2))) % width
}
}
#[derive(Debug, Clone)]
pub struct HeavyKeeperStats {
pub total_updates: u64,
pub k: usize,
pub memory_bits: u64,
pub depth: usize,
pub width: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_creation() {
let hk = HeavyKeeper::new(10, 0.001, 0.01);
assert!(hk.is_ok());
let hk = hk.unwrap();
assert_eq!(hk.k, 10);
assert!(hk.depth > 0);
assert!(hk.width > 0);
}
#[test]
fn test_single_update() {
let mut hk = HeavyKeeper::new(10, 0.001, 0.01).unwrap();
hk.update(b"test");
let count = hk.estimate(b"test");
assert!(count > 0);
}
#[test]
fn test_hash_functions() {
let hash1 = HeavyKeeper::hash_item(b"test");
let hash2 = HeavyKeeper::hash_item(b"test");
assert_eq!(hash1, hash2, "Hash should be deterministic");
let hash3 = HeavyKeeper::hash_item(b"different");
assert_ne!(hash1, hash3, "Different items should have different hashes");
}
}