use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use std::time::Duration;
use crate::Timestamp;
#[derive(Debug)]
pub struct TimeBucketedCounter<K> {
window: Duration,
bucket_width: Duration,
capacity: usize,
buckets: VecDeque<(Timestamp, HashMap<K, u64>)>,
}
impl<K> TimeBucketedCounter<K>
where
K: Hash + Eq + Clone,
{
pub fn new(window: Duration, bucket_width: Duration, capacity: usize) -> Self {
assert!(!bucket_width.is_zero(), "bucket_width must be > 0");
Self {
window,
bucket_width,
capacity,
buckets: VecDeque::new(),
}
}
pub fn new_unbounded(window: Duration, bucket_width: Duration) -> Self {
Self::new(window, bucket_width, usize::MAX)
}
pub fn bump(&mut self, key: K, now: Timestamp) {
self.evict_expired(now);
let bucket_start = self.bucket_start_for(now);
if let Some((ts, last)) = self.buckets.back_mut()
&& *ts == bucket_start
{
*last.entry(key).or_insert(0) += 1;
return;
}
let mut counts = HashMap::new();
counts.insert(key, 1);
self.buckets.push_back((bucket_start, counts));
}
pub fn count(&self, key: &K, now: Timestamp) -> u64 {
let cutoff = self.cutoff_for(now);
self.buckets
.iter()
.filter(|(ts, _)| *ts >= cutoff)
.filter_map(|(_, counts)| counts.get(key).copied())
.sum()
}
pub fn entries_above(
&self,
threshold: u64,
now: Timestamp,
) -> impl Iterator<Item = (&K, u64)> + '_ {
let cutoff = self.cutoff_for(now);
let mut totals: HashMap<&K, u64> = HashMap::new();
for (ts, counts) in &self.buckets {
if *ts < cutoff {
continue;
}
for (k, c) in counts {
*totals.entry(k).or_insert(0) += *c;
}
}
totals.into_iter().filter(move |(_, c)| *c >= threshold)
}
pub fn evict_expired(&mut self, now: Timestamp) {
let cutoff = self.cutoff_for(now);
while let Some((ts, _)) = self.buckets.front() {
if *ts < cutoff {
self.buckets.pop_front();
} else {
break;
}
}
let total_keys: usize = self.buckets.iter().map(|(_, m)| m.len()).sum();
if total_keys > self.capacity
&& let Some((_, oldest)) = self.buckets.front_mut()
{
let drop_n = total_keys.saturating_sub(self.capacity);
let mut entries: Vec<(K, u64)> = oldest.drain().collect();
entries.sort_by_key(|(_, c)| *c);
for (k, c) in entries.into_iter().skip(drop_n) {
oldest.insert(k, c);
}
}
}
pub fn len(&self) -> usize {
self.buckets.iter().map(|(_, m)| m.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.buckets.is_empty() || self.buckets.iter().all(|(_, m)| m.is_empty())
}
fn bucket_start_for(&self, ts: Timestamp) -> Timestamp {
let nanos = ts.to_duration().as_nanos();
let bw = self.bucket_width.as_nanos();
let start_nanos = (nanos / bw) * bw;
let start_dur = Duration::from_nanos(start_nanos as u64);
Timestamp::new(start_dur.as_secs() as u32, start_dur.subsec_nanos())
}
fn cutoff_for(&self, now: Timestamp) -> Timestamp {
let now_dur = now.to_duration();
let cutoff_dur = now_dur.saturating_sub(self.window);
Timestamp::new(cutoff_dur.as_secs() as u32, cutoff_dur.subsec_nanos())
}
}
impl<K> crate::correlate::Mergeable for TimeBucketedCounter<K>
where
K: Hash + Eq + Clone,
{
fn merge(&mut self, other: Self) {
assert_eq!(
self.window, other.window,
"TimeBucketedCounter::merge requires matching window",
);
assert_eq!(
self.bucket_width, other.bucket_width,
"TimeBucketedCounter::merge requires matching bucket_width",
);
assert_eq!(
self.capacity, other.capacity,
"TimeBucketedCounter::merge requires matching capacity",
);
for (ts, counts) in other.buckets {
match self.buckets.iter_mut().find(|(t, _)| *t == ts) {
Some((_, my_counts)) => {
for (k, c) in counts {
*my_counts.entry(k).or_insert(0) += c;
}
}
None => {
let pos = self
.buckets
.iter()
.position(|(t, _)| *t > ts)
.unwrap_or(self.buckets.len());
self.buckets.insert(pos, (ts, counts));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_sums_across_buckets() {
let mut c: TimeBucketedCounter<u32> =
TimeBucketedCounter::new(Duration::from_secs(60), Duration::from_secs(10), 1024);
c.bump(1, Timestamp::new(0, 0));
c.bump(1, Timestamp::new(15, 0));
c.bump(1, Timestamp::new(30, 0));
assert_eq!(c.count(&1, Timestamp::new(35, 0)), 3);
}
#[test]
fn old_buckets_evicted() {
let mut c: TimeBucketedCounter<u32> =
TimeBucketedCounter::new(Duration::from_secs(60), Duration::from_secs(10), 1024);
c.bump(1, Timestamp::new(0, 0));
c.bump(1, Timestamp::new(120, 0)); assert_eq!(c.count(&1, Timestamp::new(120, 0)), 1);
}
#[test]
fn entries_above_threshold() {
let mut c: TimeBucketedCounter<u32> =
TimeBucketedCounter::new(Duration::from_secs(60), Duration::from_secs(10), 1024);
for _ in 0..5 {
c.bump(1, Timestamp::new(0, 0));
}
for _ in 0..2 {
c.bump(2, Timestamp::new(0, 0));
}
let now = Timestamp::new(5, 0);
let big: Vec<_> = c.entries_above(3, now).map(|(k, _)| *k).collect();
assert_eq!(big, vec![1]);
}
#[test]
fn count_excludes_buckets_older_than_window() {
let mut c: TimeBucketedCounter<u32> =
TimeBucketedCounter::new(Duration::from_secs(10), Duration::from_secs(1), 1024);
c.bump(1, Timestamp::new(0, 0));
assert_eq!(c.count(&1, Timestamp::new(60, 0)), 0);
}
}