use std::{num::NonZeroUsize, sync::Arc};
use indexmap::IndexMap;
use ltk_hash::BinHash;
use crate::BinObject;
pub trait ObjectCache {
fn get(&mut self, key: BinHash) -> Option<Arc<BinObject>>;
fn put(&mut self, key: BinHash, value: Arc<BinObject>);
fn clear(&mut self);
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoCache;
impl ObjectCache for NoCache {
fn get(&mut self, _key: BinHash) -> Option<Arc<BinObject>> {
None
}
fn put(&mut self, _key: BinHash, _value: Arc<BinObject>) {}
fn clear(&mut self) {}
}
#[derive(Debug)]
pub struct LruObjectCache {
capacity: NonZeroUsize,
entries: IndexMap<BinHash, Arc<BinObject>>,
}
impl LruObjectCache {
#[must_use]
pub fn new(capacity: NonZeroUsize) -> Self {
Self {
capacity,
entries: IndexMap::new(),
}
}
#[must_use]
pub fn capacity(&self) -> NonZeroUsize {
self.capacity
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl ObjectCache for LruObjectCache {
fn get(&mut self, key: BinHash) -> Option<Arc<BinObject>> {
let index = self.entries.get_index_of(&key)?;
let (_, value) = self.entries.shift_remove_index(index)?;
self.entries.insert(key, Arc::clone(&value));
Some(value)
}
fn put(&mut self, key: BinHash, value: Arc<BinObject>) {
self.entries.shift_remove(&key);
if self.entries.len() >= self.capacity.get() {
self.entries.shift_remove_index(0);
}
self.entries.insert(key, value);
}
fn clear(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn object(path_hash: u32) -> Arc<BinObject> {
Arc::new(BinObject::new(path_hash, 0xAAAAu32))
}
fn capacity(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).expect("a non-zero capacity")
}
#[test]
fn no_cache_never_holds_anything() {
let mut cache = NoCache;
cache.put(0x1u32.into(), object(0x1));
assert!(ObjectCache::get(&mut cache, 0x1u32.into()).is_none());
}
#[test]
fn evicts_the_least_recently_used() {
let mut cache = LruObjectCache::new(capacity(2));
cache.put(0x1u32.into(), object(0x1));
cache.put(0x2u32.into(), object(0x2));
assert!(cache.get(0x1u32.into()).is_some());
cache.put(0x3u32.into(), object(0x3));
assert_eq!(cache.len(), 2);
assert!(cache.get(0x1u32.into()).is_some());
assert!(cache.get(0x2u32.into()).is_none());
assert!(cache.get(0x3u32.into()).is_some());
}
#[test]
fn an_evicted_value_stays_alive_for_its_holder() {
let mut cache = LruObjectCache::new(capacity(1));
cache.put(0x1u32.into(), object(0x1));
let held = cache.get(0x1u32.into()).expect("a hit");
cache.put(0x2u32.into(), object(0x2));
assert!(cache.get(0x1u32.into()).is_none(), "1 was evicted");
assert_eq!(held.path_hash, 0x1u32.into());
}
#[test]
fn re_inserting_a_key_does_not_grow_the_cache() {
let mut cache = LruObjectCache::new(capacity(2));
cache.put(0x1u32.into(), object(0x1));
cache.put(0x1u32.into(), object(0x1));
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
}