use alloc::vec::Vec;
use hashbrown::Equivalent;
pub(crate) struct Entry<ID, T> {
pub epoch: u64,
pub id: ID,
pub data: T,
}
pub(crate) struct LruCache<ID, T> {
entries: Vec<Entry<ID, T>>,
epoch: u64,
max_entries: usize,
}
impl<ID, T> LruCache<ID, T> {
pub(crate) fn new(max_entries: usize) -> Self {
Self {
entries: Vec::default(),
epoch: 0,
max_entries,
}
}
pub(crate) fn entry<K>(&mut self, id: K, make_data: impl FnOnce() -> T) -> &T
where
K: Equivalent<ID> + Into<ID>,
{
let index = self.find_entry(id, make_data);
self.epoch += 1;
let entry = &mut self.entries[index];
entry.epoch = self.epoch;
&entry.data
}
fn find_entry<K>(&mut self, id: K, make_data: impl FnOnce() -> T) -> usize
where
K: Equivalent<ID> + Into<ID>,
{
let epoch = self.epoch;
let mut lowest_serial = epoch;
let mut lowest_index = 0;
for (i, entry) in self.entries.iter().enumerate() {
if id.equivalent(&entry.id) {
return i;
}
if entry.epoch < lowest_serial {
lowest_serial = entry.epoch;
lowest_index = i;
}
}
if self.entries.len() < self.max_entries {
lowest_index = self.entries.len();
self.entries.push(Entry {
epoch,
id: id.into(),
data: make_data(),
});
} else {
let entry = &mut self.entries[lowest_index];
entry.epoch = epoch;
entry.id = id.into();
entry.data = make_data();
}
lowest_index
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::{String, ToString};
#[derive(Debug, Clone, PartialEq)]
struct TestId(String);
struct TestLookupKey<'a>(&'a str);
impl<'a> Equivalent<TestId> for TestLookupKey<'a> {
fn equivalent(&self, key: &TestId) -> bool {
self.0 == key.0.as_str()
}
}
impl<'a> From<TestLookupKey<'a>> for TestId {
fn from(key: TestLookupKey<'a>) -> Self {
Self(key.0.to_string())
}
}
impl Equivalent<Self> for TestId {
fn equivalent(&self, key: &Self) -> bool {
self.0 == key.0
}
}
#[test]
fn test_retrieve_existing_entry() {
let mut cache = LruCache::new(3);
let value1 = cache.entry(TestLookupKey("key1"), || 42);
assert_eq!(*value1, 42);
let value2 = cache.entry(TestLookupKey("key1"), || {
panic!("Should not create new data")
});
assert_eq!(*value2, 42);
assert_eq!(cache.entries.len(), 1);
}
#[test]
fn test_multiple_entries() {
let mut cache = LruCache::new(3);
let value1 = cache.entry(TestLookupKey("key1"), || 1);
assert_eq!(*value1, 1);
let value2 = cache.entry(TestLookupKey("key2"), || 2);
assert_eq!(*value2, 2);
let value3 = cache.entry(TestLookupKey("key3"), || 3);
assert_eq!(*value3, 3);
assert_eq!(cache.entries.len(), 3);
assert_eq!(cache.epoch, 3);
}
#[test]
fn test_lru_eviction() {
let mut cache = LruCache::new(3);
cache.entry(TestLookupKey("key1"), || 1);
cache.entry(TestLookupKey("key2"), || 2);
cache.entry(TestLookupKey("key3"), || 3);
cache.entry(TestLookupKey("key1"), || panic!("Should not create"));
cache.entry(TestLookupKey("key4"), || 4);
let value1 = cache.entry(TestLookupKey("key1"), || {
panic!("key1 should still be present")
});
assert_eq!(*value1, 1);
let mut was_created = false;
cache.entry(TestLookupKey("key2"), || {
was_created = true;
20
});
assert!(was_created, "key2 should have been evicted");
}
#[test]
fn test_lru_eviction_after_multiple_hits() {
let mut cache = LruCache::new(3);
cache.entry(TestLookupKey("key1"), || 1);
cache.entry(TestLookupKey("key2"), || 2);
cache.entry(TestLookupKey("key3"), || 3);
cache.entry(TestLookupKey("key3"), || panic!("Should not create"));
cache.entry(TestLookupKey("key1"), || panic!("Should not create"));
cache.entry(TestLookupKey("key2"), || panic!("Should not create"));
cache.entry(TestLookupKey("key4"), || 4);
let v1 = cache.entry(TestLookupKey("key1"), || {
panic!("key1 should still be present")
});
assert_eq!(*v1, 1);
let v2 = cache.entry(TestLookupKey("key2"), || {
panic!("key2 should still be present")
});
assert_eq!(*v2, 2);
let mut key3_recreated = false;
cache.entry(TestLookupKey("key3"), || {
key3_recreated = true;
30
});
assert!(key3_recreated, "key3 should have been evicted as the LRU");
}
}