use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use ahash::RandomState;
struct OccupiedEntry<K: ?Sized, V> {
key: Arc<K>,
item: Arc<V>,
prev: Option<usize>,
next: Option<usize>,
cost: u64,
}
struct EmptyEntry {
next_empty: Option<usize>,
}
enum CacheEntry<K: ?Sized, V> {
Empty(EmptyEntry),
Occupied(OccupiedEntry<K, V>),
}
impl<K: ?Sized, V> CacheEntry<K, V> {
fn as_occupied_mut(&mut self) -> &mut OccupiedEntry<K, V> {
match self {
Self::Occupied(ref mut x) => x,
_ => panic!("Entry should be occupied"),
}
}
fn as_occupied(&self) -> &OccupiedEntry<K, V> {
match self {
Self::Occupied(ref x) => x,
_ => panic!("Entry should be occupied"),
}
}
fn as_empty_mut(&mut self) -> &mut EmptyEntry {
match self {
CacheEntry::Empty(ref mut x) => x,
_ => panic!("Entry should be empty"),
}
}
}
pub struct CostBasedLru<K: ?Sized + std::hash::Hash + Eq, V> {
entries: Vec<CacheEntry<K, V>>,
index: HashMap<Arc<K>, usize, RandomState>,
max_cost: u64,
entries_head: Option<usize>,
entries_tail: Option<usize>,
empty_head: Option<usize>,
current_cost: u64,
}
impl<K: ?Sized + Hash + Eq, V> CostBasedLru<K, V> {
pub fn new(max_cost: u64) -> CostBasedLru<K, V> {
CostBasedLru {
entries: Default::default(),
index: Default::default(),
max_cost,
entries_head: None,
entries_tail: None,
empty_head: None,
current_cost: 0,
}
}
fn unlink_index(&mut self, index: usize) {
if Some(index) == self.entries_tail {
self.entries_tail = self.entries[index].as_occupied().prev;
}
if Some(index) == self.entries_head {
self.entries_head = self.entries[index].as_occupied_mut().next;
if let Some(n) = self.entries_head {
self.entries[n].as_occupied_mut().prev = None;
}
return;
}
let old_prev = self.entries[index]
.as_occupied_mut()
.prev
.expect("Isn't the head");
let old_next = self.entries[index].as_occupied_mut().next;
self.entries[old_prev].as_occupied_mut().next = old_next;
if let Some(n) = old_next {
self.entries[n].as_occupied_mut().prev = Some(old_prev);
}
}
fn make_most_recent(&mut self, index: usize) {
self.unlink_index(index);
self.entries[index].as_occupied_mut().next = self.entries_head;
if let Some(i) = self.entries_head {
self.entries[i].as_occupied_mut().prev = Some(index);
}
self.entries_head = Some(index);
if self.entries_tail.is_none() {
self.entries_tail = Some(index);
}
}
pub fn get<Q: ?Sized>(&mut self, key: &Q) -> Option<Arc<V>>
where
Arc<K>: Borrow<Q>,
Q: std::hash::Hash + Eq,
{
let ind = *self.index.get(key)?;
self.make_most_recent(ind);
Some(self.entries[ind].as_occupied_mut().item.clone())
}
fn become_empty(&mut self, index: usize) -> Arc<V> {
self.unlink_index(index);
let mut old = CacheEntry::Empty(EmptyEntry {
next_empty: self.empty_head,
});
std::mem::swap(&mut old, &mut self.entries[index]);
self.empty_head = Some(index);
match old {
CacheEntry::Occupied(OccupiedEntry {
key, item, cost, ..
}) => {
self.index.remove(&key);
self.current_cost -= cost;
item
}
_ => panic!("Should have been occupied"),
}
}
pub fn remove<Q: ?Sized>(&mut self, key: &Q) -> Option<Arc<V>>
where
Arc<K>: Borrow<Q>,
Q: std::hash::Hash + Eq,
{
let ind = self.index.remove(key)?;
let old = self.become_empty(ind);
Some(old)
}
fn find_empty(&mut self) -> usize {
if let Some(e) = self.empty_head {
self.empty_head = self.entries[e].as_empty_mut().next_empty;
return e;
}
self.entries
.push(CacheEntry::Empty(EmptyEntry { next_empty: None }));
self.entries.len() - 1
}
pub fn insert(&mut self, key: Arc<K>, value: V, cost: u64) -> Option<Arc<V>> {
let ret = self.remove(&key);
let ind = self.find_empty();
let old_head = self.entries_head;
self.entries[ind] = CacheEntry::Occupied(OccupiedEntry {
key: key.clone(),
item: Arc::new(value),
prev: None,
next: self.entries_head,
cost,
});
self.entries_head = Some(ind);
self.index.insert(key, ind);
self.current_cost += cost;
if let Some(h) = old_head {
self.entries[h].as_occupied_mut().prev = self.entries_head;
}
if self.entries_tail.is_none() {
self.entries_tail = Some(ind);
}
self.maybe_evict();
ret
}
fn maybe_evict(&mut self) {
while self.current_cost > self.max_cost {
let cur = match self.entries_tail {
Some(t) => t,
None => panic!("Not enough entries to explain cost"),
};
self.become_empty(cur);
}
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
let mut ind = self.entries_head;
std::iter::from_fn(move || {
let next = ind?;
let ret = self.entries[next].as_occupied();
ind = ret.next;
Some((&*ret.key, &*ret.item))
})
}
pub fn clear(&mut self) {
*self = Self::new(self.max_cost);
}
}
#[cfg(test)]
mod tests {
use super::*;
use lru::LruCache;
use proptest::prelude::*;
#[derive(Copy, Clone, Debug, Ord, Eq, PartialOrd, PartialEq)]
enum CacheCommand {
Put(u64, u64),
Get(u64),
Delete(u64),
}
fn cache_command_strat(
max_key: std::ops::Range<u64>,
max_value: std::ops::Range<u64>,
) -> prop::strategy::BoxedStrategy<CacheCommand> {
proptest::prop_oneof![
max_key.clone().prop_map(CacheCommand::Get),
(max_key.clone(), max_value).prop_map(|(x, y)| CacheCommand::Put(x, y)),
max_key.prop_map(CacheCommand::Delete),
]
.boxed()
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 1000,
max_shrink_iters: 100000,
..Default::default()
})]
#[test]
fn test_against_lru_cache_bounded(
bound in 1..1000u64,
commands in prop::collection::vec(cache_command_strat(0..100, 0..10000), 0..10000)
) {
let mut known_good = LruCache::<u64, u64>::new(bound as usize);
let mut ours = CostBasedLru::<u64, u64>::new(bound as u64);
for c in commands {
use CacheCommand::*;
match c {
Get(k) => {
let left: Option<u64> = known_good.get(&k).cloned();
let right: Option<u64> = ours.get(&k).as_deref().cloned();
prop_assert_eq!(left, right);
},
Put(k, v) => prop_assert_eq!(known_good.put(k, v), ours.insert(Arc::new(k), v, 1).as_deref().cloned()),
Delete(k) => prop_assert_eq!(known_good.pop(&k), ours.remove(&k).as_deref().cloned()),
}
}
}
}
#[test]
fn test_eviction() {
let mut cache = CostBasedLru::<u64, u64>::new(10);
cache.insert(Arc::new(1), 1, 1);
cache.insert(Arc::new(2), 2, 2);
cache.insert(Arc::new(3), 3, 3);
cache.insert(Arc::new(4), 4, 4);
cache.insert(Arc::new(5), 5, 5);
let state = cache
.iter()
.map(|x| (*x.0, *x.1))
.collect::<Vec<(u64, u64)>>();
assert_eq!(state, vec![(5, 5), (4, 4)]);
}
}