use super::EvictionPolicy;
use super::FrequencyEstimator;
use super::sketch::ShardedCountMinSketch;
use super::{AdmissionContext, AdmissionDecision, AdmissionPolicy, Segment};
use super::{EvictionContext, EvictionPlan};
use crate::Hash;
use std::collections::HashSet;
use std::sync::Arc;
#[derive(Debug)]
pub struct TinyLfuEstimator {
inner: ShardedCountMinSketch,
}
impl TinyLfuEstimator {
#[must_use]
pub fn new(sketch_bytes: usize) -> Self {
let cols = (sketch_bytes / 4).max(64);
Self {
inner: ShardedCountMinSketch::new(cols),
}
}
}
impl FrequencyEstimator for TinyLfuEstimator {
fn observe(&self, hash: Hash) {
self.inner.increment(&hash);
}
fn estimate(&self, hash: Hash) -> u32 {
u32::from(self.inner.estimate(&hash))
}
}
#[derive(Debug)]
pub struct ProbationAdmission {
pub freq: Arc<dyn FrequencyEstimator>,
pub promotion_threshold: u32,
}
impl AdmissionPolicy for ProbationAdmission {
fn admit(&self, ctx: &AdmissionContext) -> AdmissionDecision {
let segment = if self.freq.estimate(ctx.hash) < self.promotion_threshold {
Segment::Probation
} else {
Segment::Main
};
AdmissionDecision::Store { segment }
}
}
#[derive(Debug)]
pub struct TinyLfuEviction {
freq: Arc<dyn FrequencyEstimator>,
promotion_threshold: u32,
probation_target_pct: u64,
}
impl TinyLfuEviction {
#[must_use]
pub fn new(
freq: Arc<dyn FrequencyEstimator>,
promotion_threshold: u32,
probation_target_pct: u64,
) -> Self {
Self {
freq,
promotion_threshold,
probation_target_pct,
}
}
}
impl EvictionPolicy for TinyLfuEviction {
fn plan(&self, ctx: &EvictionContext<'_>) -> EvictionPlan {
let size = |h: &Hash| ctx.sizes.get(h).copied().unwrap_or(0);
let mut promote = Vec::new();
let mut promoted: HashSet<Hash> = HashSet::new();
for (h, seg) in ctx.segments {
if *seg == Segment::Probation && self.freq.estimate(*h) >= self.promotion_threshold {
promote.push((*h, Segment::Main));
promoted.insert(*h);
}
}
let mut evict = Vec::new();
let mut evicted: HashSet<Hash> = HashSet::new();
let mut freed = 0u64;
let probation_limit = ctx
.cache_bytes
.saturating_mul(self.probation_target_pct)
.saturating_div(100);
let mut probation_members: Vec<(Hash, u32, std::time::Instant)> = ctx
.candidates
.iter()
.filter(|(h, _)| {
ctx.segments.get(*h).copied() == Some(Segment::Probation) && !promoted.contains(*h)
})
.map(|(h, t)| (*h, self.freq.estimate(*h), *t))
.collect();
let probation_footprint: u64 = probation_members
.iter()
.map(|(h, _, _)| ctx.sizes.get(h).copied().unwrap_or(0))
.sum();
if probation_footprint > probation_limit {
probation_members.sort_by(|a, b| {
a.1.cmp(&b.1)
.then(a.2.cmp(&b.2))
.then_with(|| size(&b.0).cmp(&size(&a.0)))
});
let mut remaining = probation_footprint;
for (h, _, _) in probation_members {
if remaining <= probation_limit {
break;
}
if evict.len() as u64 >= ctx.budget {
break;
}
let size = ctx.sizes.get(&h).copied().unwrap_or(0);
remaining = remaining.saturating_sub(size);
freed = freed.saturating_add(size);
evicted.insert(h);
evict.push(h);
}
}
let mut scored: Vec<(Hash, u32, std::time::Instant)> = ctx
.candidates
.iter()
.filter(|(h, _)| !evicted.contains(*h) && !promoted.contains(*h))
.map(|(h, t)| (*h, self.freq.estimate(*h), *t))
.collect();
scored.sort_by(|a, b| {
a.1.cmp(&b.1)
.then(a.2.cmp(&b.2))
.then_with(|| size(&b.0).cmp(&size(&a.0)))
});
for (h, _, _) in scored {
if ctx.total_bytes.saturating_sub(freed) <= ctx.target_bytes {
break;
}
if evict.len() as u64 >= ctx.budget {
break;
}
freed = freed.saturating_add(ctx.sizes.get(&h).copied().unwrap_or(0));
evict.push(h);
}
EvictionPlan { evict, promote }
}
}
#[cfg(test)]
mod tests;