use crate::{Entry, EvictionPolicy, Store};
use std::time::Instant;
const N_SAMPLES: usize = 5;
const HEADROOM_NUM: u64 = 19;
const HEADROOM_DEN: u64 = 20;
const MAX_EVICTIONS_PER_CALL: usize = 1_000_000;
const LFU_INIT_VAL: u8 = 5;
const LFU_LOG_FACTOR: u32 = 10;
const LFU_COUNTER_MAX: u8 = 255;
#[inline]
pub(crate) fn touch_on_access(e: &mut Entry, policy: EvictionPolicy, clock: u32) {
if policy.uses_lru() {
e.set_lru_clock(clock);
} else if policy.uses_lfu() {
let cur = e.lru_clock();
let counter = cur as u8;
let next = lfu_log_incr(counter, clock);
e.set_lru_clock((cur & 0xFFFF_FF00) | next as u32);
}
}
#[inline]
fn lfu_log_incr(counter: u8, clock: u32) -> u8 {
if counter == LFU_COUNTER_MAX {
return counter;
}
let baseval = counter.saturating_sub(LFU_INIT_VAL) as u32;
let threshold = baseval.saturating_mul(LFU_LOG_FACTOR).saturating_add(1);
if threshold == 1 || splitmix32(clock).is_multiple_of(threshold) {
counter.saturating_add(1)
} else {
counter
}
}
#[inline]
fn splitmix32(x: u32) -> u32 {
let mut z = x.wrapping_add(0x9E37_79B9);
z = (z ^ (z >> 16)).wrapping_mul(0x85EB_CA6B);
z = (z ^ (z >> 13)).wrapping_mul(0xC2B2_AE35);
z ^ (z >> 16)
}
pub(crate) fn evict_until_under_limit(store: &mut Store) -> usize {
if store.eviction_policy == EvictionPolicy::NoEviction {
return 0;
}
let target = store
.maxmemory
.saturating_mul(HEADROOM_NUM)
/ HEADROOM_DEN;
let mut evicted = 0;
let mut consecutive_no_candidate = 0;
while store.used_memory > target {
if evict_one(store) {
evicted += 1;
consecutive_no_candidate = 0;
} else {
consecutive_no_candidate += 1;
if consecutive_no_candidate >= 3 {
break;
}
}
if evicted >= MAX_EVICTIONS_PER_CALL {
break;
}
}
evicted
}
fn evict_one(store: &mut Store) -> bool {
let Some(victim) = sample_and_pick(store, N_SAMPLES) else {
return false;
};
if store.remove_entry(&victim).is_some() {
store.evictions_total += 1;
true
} else {
false
}
}
fn sample_and_pick(store: &mut Store, n: usize) -> Option<Vec<u8>> {
let cap = store.map.capacity();
if cap == 0 || store.map.is_empty() {
return None;
}
let policy = store.eviction_policy;
let volatile_only = policy.is_volatile();
let now = Instant::now();
let clock = store.clock_counter as u32;
let start = (splitmix32(clock) as usize) % cap;
let mut best: Option<(Vec<u8>, i64)> = None;
let mut taken = 0;
let visit_cap = cap.saturating_mul(2);
let primary = store.map.iter_from_bucket(start);
let wrap = store.map.iter_from_bucket(0);
for (k, e) in primary.chain(wrap).take(visit_cap) {
if volatile_only && e.expire_at_ns.is_none() {
continue;
}
let score = score_entry(e, policy, now, clock);
if best.as_ref().is_none_or(|(_, bs)| score < *bs) {
best = Some((k.to_vec(), score));
}
taken += 1;
if taken >= n {
break;
}
}
best.map(|(k, _)| k)
}
#[inline]
fn score_entry(e: &Entry, policy: EvictionPolicy, now: Instant, clock: u32) -> i64 {
match policy {
EvictionPolicy::AllKeysLru | EvictionPolicy::VolatileLru => e.lru_clock() as i64,
EvictionPolicy::AllKeysLfu | EvictionPolicy::VolatileLfu => {
(e.lru_clock() & 0xFF) as i64
}
EvictionPolicy::AllKeysRandom | EvictionPolicy::VolatileRandom => {
splitmix32(clock ^ e.lru_clock()) as i64
}
EvictionPolicy::VolatileTtl => match e.expire_at_ns {
Some(ns) => crate::unpack_deadline(ns)
.saturating_duration_since(now)
.as_millis() as i64,
None => i64::MAX, },
EvictionPolicy::NoEviction => i64::MAX,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lfu_increment_eventually_saturates() {
let mut c = LFU_INIT_VAL;
for clock in 0..1_000_000u32 {
c = lfu_log_incr(c, clock);
}
assert!(c >= LFU_INIT_VAL + 5, "expected meaningful growth, got {c}");
}
#[test]
fn lfu_increment_is_log_scaled() {
let mut low_hits = 0u32;
let mut high_hits = 0u32;
for clock in 0..10_000u32 {
if lfu_log_incr(LFU_INIT_VAL, clock) > LFU_INIT_VAL {
low_hits += 1;
}
if lfu_log_incr(200, clock) > 200 {
high_hits += 1;
}
}
assert!(low_hits > 100 * high_hits,
"log scaling broken: low={low_hits} high={high_hits}");
}
#[test]
fn splitmix_avalanches_neighbours() {
let a = splitmix32(0);
let b = splitmix32(1);
let diff = (a ^ b).count_ones();
assert!(diff > 10, "splitmix didn't avalanche: a={a:x} b={b:x}");
}
}