use yo_common::Addr;
use crate::access::{Lfu, Policy};
use crate::value;
pub const SAMPLES: usize = 5;
pub const ANY: u64 = u64::MAX;
#[must_use]
pub fn eligible(rec: &[u8], policy: Policy) -> bool {
!policy.volatile_only() || value::expire_at(rec).is_some()
}
#[must_use]
pub fn score(rec: &[u8], policy: Policy, now_ms: u64, lfu: Lfu) -> u64 {
if matches!(policy, Policy::VolatileTtl) {
return value::expire_at(rec).map_or(0, |at| u64::MAX - at);
}
if policy.is_random() {
return ANY;
}
let access = value::access(rec).unwrap_or_default();
if policy.is_lfu() {
return u64::from(u8::MAX - access.freq(now_ms, lfu));
}
access.idle_secs(now_ms)
}
#[derive(Debug, Clone, Copy)]
pub struct Best {
pub addr: Addr,
pub score: u64,
}
impl Best {
pub const EMPTY: Best = Best {
addr: Addr::NONE,
score: 0,
};
#[must_use]
pub const fn is_empty(self) -> bool {
self.addr.is_none()
}
pub fn offer(&mut self, addr: Addr, score: u64) {
if self.is_empty() || score > self.score {
*self = Best { addr, score };
}
}
}
pub const CANDIDATES: usize = 16;
#[derive(Debug, Default, Clone)]
struct Slot {
score: u64,
key: Vec<u8>,
}
impl Slot {
fn fill(&mut self, key: &[u8], score: u64) {
self.score = score;
self.key.clear();
self.key.extend_from_slice(key);
}
}
#[derive(Debug, Default, Clone)]
pub struct Pool {
at: Vec<Slot>,
len: usize,
}
impl Pool {
#[must_use]
pub const fn new() -> Pool {
Pool {
at: Vec::new(),
len: 0,
}
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn clear(&mut self) {
self.len = 0;
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.at.capacity() * size_of::<Slot>()
+ self.at.iter().map(|s| s.key.capacity()).sum::<usize>()
}
pub fn offer(&mut self, key: &[u8], score: u64) {
if self.at.is_empty() {
self.at.resize_with(CANDIDATES, Slot::default);
}
if let Some(i) = self.at[..self.len].iter().position(|s| s.key == key) {
if self.at[i].score == score {
return;
}
self.at[i..self.len].rotate_left(1);
self.len -= 1;
} else if self.len == CANDIDATES && score <= self.at[0].score {
return;
}
let i = self.at[..self.len].partition_point(|s| s.score <= score);
let at = if self.len < CANDIDATES {
self.at[i..=self.len].rotate_right(1);
self.len += 1;
i
} else {
self.at[..i].rotate_left(1);
i - 1
};
self.at[at].fill(key, score);
}
pub fn take(&mut self) -> Option<&[u8]> {
if self.len == 0 {
return None;
}
self.len -= 1;
Some(&self.at[self.len].key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn held(p: &Pool) -> Vec<(&[u8], u64)> {
p.at[..p.len]
.iter()
.rev()
.map(|s| (&s.key[..], s.score))
.collect()
}
#[test]
fn the_worst_candidate_comes_out_first() {
let mut p = Pool::new();
p.offer(b"middling", 50);
p.offer(b"terrible", 90);
p.offer(b"fine", 10);
assert_eq!(p.len(), 3);
assert_eq!(p.take(), Some(&b"terrible"[..]));
assert_eq!(p.take(), Some(&b"middling"[..]));
assert_eq!(p.take(), Some(&b"fine"[..]));
assert_eq!(p.take(), None);
assert!(p.is_empty());
}
#[test]
fn a_full_pool_keeps_the_worst_sixteen_and_nothing_else() {
let mut p = Pool::new();
for i in 0..32u64 {
p.offer(format!("key-{i}").as_bytes(), 1000 - i);
}
assert_eq!(p.len(), CANDIDATES);
let names: Vec<_> = held(&p)
.into_iter()
.map(|(k, _)| String::from_utf8(k.to_vec()).expect("ascii"))
.collect();
assert_eq!(names[0], "key-0", "the worst key offered");
assert_eq!(names[15], "key-15");
let mut q = Pool::new();
for i in 0..32u64 {
q.offer(format!("key-{i}").as_bytes(), i);
}
assert_eq!(q.len(), CANDIDATES);
assert_eq!(q.take(), Some(&b"key-31"[..]), "the worst key offered");
}
#[test]
fn a_key_offered_twice_is_held_once_at_its_new_score() {
let mut p = Pool::new();
p.offer(b"a", 10);
p.offer(b"b", 20);
p.offer(b"c", 30);
p.offer(b"a", 40);
assert_eq!(p.len(), 3);
assert_eq!(
held(&p),
vec![(&b"a"[..], 40), (&b"c"[..], 30), (&b"b"[..], 20)]
);
}
#[test]
fn a_key_offered_twice_at_the_same_score_changes_nothing() {
let mut p = Pool::new();
p.offer(b"a", 10);
p.offer(b"b", 20);
p.offer(b"a", 10);
assert_eq!(p.len(), 2);
assert_eq!(held(&p), vec![(&b"b"[..], 20), (&b"a"[..], 10)]);
}
#[test]
fn a_key_offered_twice_into_a_full_pool_still_leaves_room() {
let mut p = Pool::new();
for i in 0..CANDIDATES as u64 {
p.offer(format!("key-{i}").as_bytes(), 100 + i);
}
p.offer(b"key-3", 999);
assert_eq!(p.len(), CANDIDATES);
assert_eq!(p.take(), Some(&b"key-3"[..]));
assert_eq!(
p.take(),
Some(&b"key-15"[..]),
"the front was not thrown away"
);
}
#[test]
fn clearing_forgets_the_candidates_and_keeps_the_buffers() {
let mut p = Pool::new();
for i in 0..CANDIDATES as u64 {
p.offer(format!("a rather long key name number {i}").as_bytes(), i);
}
let held = p.memory_bytes();
p.clear();
assert!(p.is_empty());
assert_eq!(p.take(), None);
assert_eq!(p.memory_bytes(), held, "the buffers went with the scores");
}
#[test]
fn a_warm_pool_does_not_allocate_again() {
let mut p = Pool::new();
for i in 0..64u64 {
p.offer(format!("key-{i:0>6}").as_bytes(), i % 17);
}
let settled = p.memory_bytes();
for i in 0..1000u64 {
p.offer(format!("key-{i:0>6}").as_bytes(), i % 17);
}
assert_eq!(
p.memory_bytes(),
settled,
"a key no longer than any it has seen cost it an allocation"
);
}
#[test]
fn an_untouched_pool_costs_nothing() {
let p = Pool::new();
assert_eq!(p.memory_bytes(), 0);
assert!(p.is_empty());
}
}