use crate::common::{Mergeable, Sketch, SketchError};
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone)]
pub struct SlidingHyperLogLog {
precision: u8,
registers: Vec<RegisterWithTime>,
max_window_seconds: u64,
metadata: SlidingHLLMetadata,
}
#[derive(Clone, Debug)]
struct RegisterWithTime {
value: u8,
timestamp: u64,
}
impl RegisterWithTime {
fn new() -> Self {
RegisterWithTime {
value: 0,
timestamp: 0,
}
}
}
#[derive(Clone, Debug)]
struct SlidingHLLMetadata {
last_decay_time: u64,
total_updates: u64,
}
impl SlidingHyperLogLog {
pub const MIN_PRECISION: u8 = 4;
pub const MAX_PRECISION: u8 = 16;
pub fn new(precision: u8, max_window_seconds: u64) -> Result<Self, SketchError> {
if !(Self::MIN_PRECISION..=Self::MAX_PRECISION).contains(&precision) {
return Err(SketchError::InvalidParameter {
param: "precision".to_string(),
value: precision.to_string(),
constraint: format!(
"must be between {} and {}",
Self::MIN_PRECISION,
Self::MAX_PRECISION
),
});
}
let m = 1usize << precision;
let registers = vec![RegisterWithTime::new(); m];
Ok(SlidingHyperLogLog {
precision,
registers,
max_window_seconds,
metadata: SlidingHLLMetadata {
last_decay_time: 0,
total_updates: 0,
},
})
}
pub fn update<T: Hash>(&mut self, item: &T, timestamp: u64) -> Result<(), SketchError> {
let hash = self.hash_item(item);
self.update_hash(hash, timestamp);
self.metadata.total_updates += 1;
Ok(())
}
#[inline(always)]
fn hash_item<T: Hash>(&self, item: &T) -> u64 {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
hasher.finish()
}
#[inline]
fn update_hash(&mut self, hash: u64, timestamp: u64) {
let idx = (hash >> (64 - self.precision)) as usize;
let w = hash << self.precision | (1u64 << (self.precision - 1));
let rho = (w.leading_zeros() + 1) as u8;
let register = &mut self.registers[idx];
if rho > register.value || (rho == register.value && timestamp > register.timestamp) {
register.value = rho;
register.timestamp = timestamp;
}
}
pub fn estimate_window(&self, current_time: u64, window_seconds: u64) -> f64 {
let cutoff_time = current_time.saturating_sub(window_seconds);
let m = self.registers.len() as f64;
let mut sum = 0.0;
let mut zeros = 0;
for register in &self.registers {
if register.value > 0
&& register.timestamp >= cutoff_time
&& register.timestamp <= current_time
{
sum += 2.0_f64.powi(-(register.value as i32));
} else {
zeros += 1;
sum += 1.0; }
}
let alpha_m = self.alpha();
let raw_estimate = alpha_m * m * m / sum;
if raw_estimate <= 2.5 * m && zeros > 0 {
return m * (m / zeros as f64).ln();
}
raw_estimate
}
pub fn estimate_total(&self) -> f64 {
let m = self.registers.len() as f64;
let mut sum = 0.0;
let mut zeros = 0;
for register in &self.registers {
if register.value > 0 {
sum += 2.0_f64.powi(-(register.value as i32));
} else {
zeros += 1;
sum += 1.0;
}
}
let alpha_m = self.alpha();
let raw_estimate = alpha_m * m * m / sum;
if raw_estimate <= 2.5 * m && zeros > 0 {
return m * (m / zeros as f64).ln();
}
raw_estimate
}
pub fn decay(&mut self, current_time: u64, window_seconds: u64) -> Result<(), SketchError> {
let cutoff_time = current_time.saturating_sub(window_seconds);
for register in &mut self.registers {
if register.timestamp < cutoff_time {
register.value = 0;
register.timestamp = 0;
}
}
self.metadata.last_decay_time = current_time;
Ok(())
}
fn alpha(&self) -> f64 {
let m = self.registers.len() as f64;
match self.registers.len() {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / m),
}
}
pub fn stats(&self) -> SlidingHLLStats {
SlidingHLLStats {
precision: self.precision,
max_window_seconds: self.max_window_seconds,
total_updates: self.metadata.total_updates,
}
}
#[inline]
pub fn precision(&self) -> u8 {
self.precision
}
#[inline]
pub fn num_registers(&self) -> usize {
1 << self.precision
}
pub fn standard_error(&self) -> f64 {
1.04 / (self.num_registers() as f64).sqrt()
}
pub fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(self.precision);
bytes.extend_from_slice(&self.max_window_seconds.to_le_bytes());
for register in &self.registers {
bytes.push(register.value);
bytes.extend_from_slice(®ister.timestamp.to_le_bytes());
}
bytes.extend_from_slice(&self.metadata.last_decay_time.to_le_bytes());
bytes.extend_from_slice(&self.metadata.total_updates.to_le_bytes());
bytes
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 9 {
return Err(SketchError::DeserializationError(
"Insufficient bytes".to_string(),
));
}
let precision = bytes[0];
if !(Self::MIN_PRECISION..=Self::MAX_PRECISION).contains(&precision) {
return Err(SketchError::DeserializationError(format!(
"Invalid precision: {}",
precision
)));
}
let max_window_seconds =
u64::from_le_bytes(bytes[1..9].try_into().map_err(|_| {
SketchError::DeserializationError("Invalid window size".to_string())
})?);
let m = 1usize << precision;
let expected_len = 9 + m * 9 + 16;
if bytes.len() != expected_len {
return Err(SketchError::DeserializationError(format!(
"Expected {} bytes, got {}",
expected_len,
bytes.len()
)));
}
let mut registers = Vec::with_capacity(m);
let mut offset = 9;
for _ in 0..m {
let value = bytes[offset];
let timestamp =
u64::from_le_bytes(bytes[offset + 1..offset + 9].try_into().map_err(|_| {
SketchError::DeserializationError("Invalid timestamp".to_string())
})?);
registers.push(RegisterWithTime { value, timestamp });
offset += 9;
}
let last_decay_time = u64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("Invalid metadata".to_string()))?,
);
let total_updates = u64::from_le_bytes(
bytes[offset + 8..offset + 16]
.try_into()
.map_err(|_| SketchError::DeserializationError("Invalid metadata".to_string()))?,
);
Ok(SlidingHyperLogLog {
precision,
registers,
max_window_seconds,
metadata: SlidingHLLMetadata {
last_decay_time,
total_updates,
},
})
}
}
impl Sketch for SlidingHyperLogLog {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
let _ = self.update(item, 0);
}
fn estimate(&self) -> f64 {
self.estimate_total()
}
fn is_empty(&self) -> bool {
self.registers.iter().all(|r| r.value == 0)
}
fn serialize(&self) -> Vec<u8> {
self.serialize()
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
Self::deserialize(bytes)
}
}
impl Mergeable for SlidingHyperLogLog {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.precision != other.precision {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Precision mismatch: {} vs {}",
self.precision, other.precision
),
});
}
if self.max_window_seconds != other.max_window_seconds {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Window size mismatch: {} vs {}",
self.max_window_seconds, other.max_window_seconds
),
});
}
for (i, other_reg) in other.registers.iter().enumerate() {
let self_reg = &mut self.registers[i];
if other_reg.value > self_reg.value {
self_reg.value = other_reg.value;
self_reg.timestamp = other_reg.timestamp;
} else if other_reg.value == self_reg.value && other_reg.timestamp > self_reg.timestamp
{
self_reg.timestamp = other_reg.timestamp;
}
}
self.metadata.total_updates += other.metadata.total_updates;
self.metadata.last_decay_time = self
.metadata
.last_decay_time
.max(other.metadata.last_decay_time);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SlidingHLLStats {
pub precision: u8,
pub max_window_seconds: u64,
pub total_updates: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_sliding_hll() {
let hll = SlidingHyperLogLog::new(12, 3600).unwrap();
let stats = hll.stats();
assert_eq!(stats.precision, 12);
assert_eq!(stats.max_window_seconds, 3600);
assert_eq!(stats.total_updates, 0);
}
#[test]
fn test_invalid_precision() {
assert!(SlidingHyperLogLog::new(3, 3600).is_err());
assert!(SlidingHyperLogLog::new(17, 3600).is_err());
}
#[test]
fn test_update_and_estimate() {
let mut hll = SlidingHyperLogLog::new(12, 3600).unwrap();
for i in 0..100 {
hll.update(&i, 1000).unwrap();
}
let estimate = hll.estimate_total();
assert!(
(estimate - 100.0).abs() < 30.0,
"Estimate {} too far from 100",
estimate
);
}
#[test]
fn test_window_estimation() {
let mut hll = SlidingHyperLogLog::new(12, 3600).unwrap();
for i in 0..50 {
hll.update(&i, 1000).unwrap();
}
for i in 50..100 {
hll.update(&i, 2000).unwrap();
}
let estimate = hll.estimate_window(2500, 600);
assert!(estimate >= 20.0, "Expected items in window");
}
#[test]
fn test_serialization() {
let mut hll = SlidingHyperLogLog::new(12, 3600).unwrap();
for i in 0..100 {
hll.update(&i, 1000).unwrap();
}
let bytes = hll.serialize();
let restored = SlidingHyperLogLog::deserialize(&bytes).unwrap();
assert_eq!(hll.precision, restored.precision);
assert_eq!(hll.max_window_seconds, restored.max_window_seconds);
}
}