extern crate alloc;
use crate::config::LfudaCacheConfig;
use crate::entry::CacheEntry;
use crate::list::{List, ListEntry};
use crate::metrics::{CacheMetrics, LfudaCacheMetrics};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LfudaMeta {
pub frequency: u64,
pub age_at_insertion: u64,
}
impl LfudaMeta {
#[inline]
pub fn new(frequency: u64, age_at_insertion: u64) -> Self {
Self {
frequency,
age_at_insertion,
}
}
#[inline]
pub fn increment(&mut self) -> u64 {
self.frequency += 1;
self.frequency
}
#[inline]
pub fn priority(&self) -> u64 {
self.frequency + self.age_at_insertion
}
}
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::borrow::Borrow;
use core::hash::{BuildHasher, Hash};
use core::num::NonZeroUsize;
#[cfg(feature = "hashbrown")]
use hashbrown::DefaultHashBuilder;
#[cfg(feature = "hashbrown")]
use hashbrown::HashMap;
#[cfg(not(feature = "hashbrown"))]
use std::collections::hash_map::RandomState as DefaultHashBuilder;
#[cfg(not(feature = "hashbrown"))]
use std::collections::HashMap;
pub(crate) struct LfudaSegment<K, V, S = DefaultHashBuilder> {
config: LfudaCacheConfig,
global_age: u64,
min_priority: u64,
map: HashMap<K, *mut ListEntry<CacheEntry<K, V, LfudaMeta>>, S>,
priority_lists: BTreeMap<u64, List<CacheEntry<K, V, LfudaMeta>>>,
metrics: LfudaCacheMetrics,
current_size: u64,
}
unsafe impl<K: Send, V: Send, S: Send> Send for LfudaSegment<K, V, S> {}
unsafe impl<K: Send, V: Send, S: Sync> Sync for LfudaSegment<K, V, S> {}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaSegment<K, V, S> {
#[allow(dead_code)] pub(crate) fn init(config: LfudaCacheConfig, hasher: S) -> Self {
let map_capacity = config.capacity.get().next_power_of_two();
LfudaSegment {
config,
global_age: config.initial_age as u64,
min_priority: 0,
map: HashMap::with_capacity_and_hasher(map_capacity, hasher),
priority_lists: BTreeMap::new(),
metrics: LfudaCacheMetrics::new(config.max_size),
current_size: 0,
}
}
#[inline]
pub(crate) fn cap(&self) -> NonZeroUsize {
self.config.capacity
}
#[inline]
pub(crate) fn len(&self) -> usize {
self.map.len()
}
#[inline]
pub(crate) fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[inline]
pub(crate) fn global_age(&self) -> u64 {
self.global_age
}
#[inline]
pub(crate) fn current_size(&self) -> u64 {
self.current_size
}
#[inline]
pub(crate) fn max_size(&self) -> u64 {
self.config.max_size
}
#[inline]
pub(crate) fn metrics(&self) -> &LfudaCacheMetrics {
&self.metrics
}
#[inline]
pub(crate) fn record_miss(&mut self, object_size: u64) {
self.metrics.core.record_miss(object_size);
}
unsafe fn update_priority_by_node(
&mut self,
node: *mut ListEntry<CacheEntry<K, V, LfudaMeta>>,
old_priority: u64,
) -> *mut ListEntry<CacheEntry<K, V, LfudaMeta>>
where
K: Clone + Hash + Eq,
{
let entry = (*node).get_value();
let key_cloned = entry.key.clone();
let node = *self.map.get(&key_cloned).unwrap();
let meta = &(*node).get_value().metadata.algorithm;
let new_priority = (meta.frequency + 1) + meta.age_at_insertion;
if old_priority == new_priority {
self.priority_lists
.get_mut(&old_priority)
.unwrap()
.move_to_front(node);
return node;
}
let boxed_entry = self
.priority_lists
.get_mut(&old_priority)
.unwrap()
.remove(node)
.unwrap();
if self.priority_lists.get(&old_priority).unwrap().is_empty() {
self.priority_lists.remove(&old_priority);
if old_priority == self.min_priority {
self.min_priority = new_priority;
}
}
let entry_ptr = Box::into_raw(boxed_entry);
(*entry_ptr).get_value_mut().metadata.algorithm.frequency += 1;
let capacity = self.config.capacity;
self.priority_lists
.entry(new_priority)
.or_insert_with(|| List::new(capacity));
self.priority_lists
.get_mut(&new_priority)
.unwrap()
.attach_from_other_list(entry_ptr);
*self.map.get_mut(&key_cloned).unwrap() = entry_ptr;
entry_ptr
}
pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q> + Clone,
Q: ?Sized + Hash + Eq,
{
if let Some(&node) = self.map.get(key) {
unsafe {
let entry = (*node).get_value();
let meta = &entry.metadata.algorithm;
let old_priority = meta.priority();
self.metrics.core.record_hit(entry.metadata.size);
let new_node = self.update_priority_by_node(node, old_priority);
let new_entry = (*new_node).get_value();
Some(&new_entry.value)
}
} else {
None
}
}
pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q> + Clone,
Q: ?Sized + Hash + Eq,
{
if let Some(&node) = self.map.get(key) {
unsafe {
let entry = (*node).get_value();
let meta = &entry.metadata.algorithm;
let old_priority = meta.priority();
self.metrics.core.record_hit(entry.metadata.size);
let new_priority = (meta.frequency + 1) + meta.age_at_insertion;
self.metrics.record_frequency_increment(new_priority);
let new_node = self.update_priority_by_node(node, old_priority);
let new_entry = (*new_node).get_value_mut();
Some(&mut new_entry.value)
}
} else {
None
}
}
pub(crate) fn put(&mut self, key: K, value: V, size: u64) -> Option<Vec<(K, V)>>
where
K: Clone,
{
if let Some(&node) = self.map.get(&key) {
unsafe {
let entry = (*node).get_value();
let meta = &entry.metadata.algorithm;
let priority = meta.priority();
let old_size = entry.metadata.size;
let new_entry = CacheEntry::with_algorithm_metadata(
key.clone(),
value,
size,
LfudaMeta::new(meta.frequency, meta.age_at_insertion),
);
let _old_entry = self
.priority_lists
.get_mut(&priority)
.unwrap()
.update(node, new_entry, true);
self.current_size = self.current_size.saturating_sub(old_size);
self.current_size += size;
self.metrics.core.record_size_change(old_size, size);
self.metrics.core.bytes_written_to_cache += size;
return None;
}
}
let mut evicted = Vec::new();
let frequency: u64 = 1;
let age_at_insertion = self.global_age;
let priority = frequency + age_at_insertion;
while self.len() >= self.config.capacity.get()
|| (self.current_size + size > self.config.max_size && !self.map.is_empty())
{
if let Some(entry) = self.evict() {
self.metrics.core.evictions += 1;
evicted.push(entry);
} else {
break;
}
}
self.min_priority = if self.is_empty() {
priority
} else {
core::cmp::min(self.min_priority, priority)
};
let capacity = self.config.capacity;
self.priority_lists
.entry(priority)
.or_insert_with(|| List::new(capacity));
let cache_entry = CacheEntry::with_algorithm_metadata(
key.clone(),
value,
size,
LfudaMeta::new(frequency, age_at_insertion),
);
if let Some(node) = self
.priority_lists
.get_mut(&priority)
.unwrap()
.add(cache_entry)
{
self.map.insert(key, node);
self.current_size += size;
self.metrics.core.record_insertion(size);
self.metrics.record_frequency_increment(priority);
if age_at_insertion > 0 {
self.metrics.record_aging_benefit(age_at_insertion);
}
}
if evicted.is_empty() {
None
} else {
Some(evicted)
}
}
pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let node = self.map.remove(key)?;
unsafe {
let priority = (*node).get_value().metadata.algorithm.priority();
let boxed_entry = self.priority_lists.get_mut(&priority)?.remove(node)?;
let entry_ptr = Box::into_raw(boxed_entry);
let cache_entry = (*entry_ptr).take_value();
let removed_size = cache_entry.metadata.size;
let _ = Box::from_raw(entry_ptr);
self.current_size = self.current_size.saturating_sub(removed_size);
self.metrics.core.record_removal(removed_size);
if self.priority_lists.get(&priority).unwrap().is_empty() {
self.priority_lists.remove(&priority);
if priority == self.min_priority {
self.min_priority = self
.priority_lists
.keys()
.copied()
.next()
.unwrap_or(self.global_age);
}
}
Some(cache_entry.value)
}
}
pub(crate) fn clear(&mut self) {
self.map.clear();
self.priority_lists.clear();
self.global_age = 0;
self.min_priority = 0;
self.current_size = 0;
}
#[inline]
pub(crate) fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.map.contains_key(key)
}
pub(crate) fn peek<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let &node = self.map.get(key)?;
unsafe {
let entry = (*node).get_value();
Some(&entry.value)
}
}
fn evict(&mut self) -> Option<(K, V)> {
if self.is_empty() {
return None;
}
let min_key = loop {
if let Some(min_priority_list) = self.priority_lists.get(&self.min_priority) {
if !min_priority_list.is_empty() {
break self.min_priority;
}
let stale = self.min_priority;
self.priority_lists.remove(&stale);
self.min_priority = self
.priority_lists
.keys()
.copied()
.next()
.unwrap_or(self.global_age);
} else {
self.min_priority = self
.priority_lists
.keys()
.copied()
.next()
.unwrap_or(self.global_age);
if self.priority_lists.is_empty() {
return None;
}
}
};
let min_priority_list = self.priority_lists.get_mut(&min_key)?;
let old_entry = min_priority_list.remove_last()?;
let is_list_empty = min_priority_list.is_empty();
unsafe {
let entry_ptr = Box::into_raw(old_entry);
let cache_entry = (*entry_ptr).take_value();
let evicted_size = cache_entry.metadata.size;
let evicted_priority = cache_entry.metadata.algorithm.priority();
self.global_age = evicted_priority;
self.metrics.record_aging_event(self.global_age);
self.map.remove(&cache_entry.key);
self.current_size = self.current_size.saturating_sub(evicted_size);
self.metrics.core.record_removal(evicted_size);
if is_list_empty {
self.priority_lists.remove(&self.min_priority);
self.min_priority = self
.priority_lists
.keys()
.copied()
.next()
.unwrap_or(self.global_age);
}
let _ = Box::from_raw(entry_ptr);
Some((cache_entry.key, cache_entry.value))
}
}
}
impl<K, V, S> core::fmt::Debug for LfudaSegment<K, V, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LfudaSegment")
.field("capacity", &self.config.capacity)
.field("len", &self.map.len())
.field("global_age", &self.global_age)
.field("min_priority", &self.min_priority)
.finish()
}
}
#[derive(Debug)]
pub struct LfudaCache<K, V, S = DefaultHashBuilder> {
segment: LfudaSegment<K, V, S>,
}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaCache<K, V, S> {
#[inline]
pub fn cap(&self) -> NonZeroUsize {
self.segment.cap()
}
#[inline]
pub fn len(&self) -> usize {
self.segment.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.segment.is_empty()
}
#[inline]
pub fn current_size(&self) -> u64 {
self.segment.current_size()
}
#[inline]
pub fn max_size(&self) -> u64 {
self.segment.max_size()
}
#[inline]
pub fn global_age(&self) -> u64 {
self.segment.global_age()
}
#[inline]
pub fn record_miss(&mut self, object_size: u64) {
self.segment.record_miss(object_size);
}
#[inline]
pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q> + Clone,
Q: ?Sized + Hash + Eq,
{
self.segment.get(key)
}
#[inline]
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q> + Clone,
Q: ?Sized + Hash + Eq,
{
self.segment.get_mut(key)
}
#[inline]
pub fn put(&mut self, key: K, value: V, size: u64) -> Option<Vec<(K, V)>>
where
K: Clone,
{
self.segment.put(key, value, size)
}
#[inline]
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
V: Clone,
{
self.segment.remove(key)
}
#[inline]
pub fn clear(&mut self) {
self.segment.clear()
}
#[inline]
pub fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.contains(key)
}
#[inline]
pub fn peek<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.peek(key)
}
}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> CacheMetrics for LfudaCache<K, V, S> {
fn metrics(&self) -> BTreeMap<String, f64> {
self.segment.metrics().metrics()
}
fn algorithm_name(&self) -> &'static str {
self.segment.metrics().algorithm_name()
}
}
impl<K: Hash + Eq, V> LfudaCache<K, V>
where
V: Clone,
{
pub fn init(
config: LfudaCacheConfig,
hasher: Option<DefaultHashBuilder>,
) -> LfudaCache<K, V, DefaultHashBuilder> {
LfudaCache {
segment: LfudaSegment::init(config, hasher.unwrap_or_default()),
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
use alloc::string::ToString;
use super::*;
use crate::config::LfudaCacheConfig;
use alloc::string::String;
fn make_cache<K: Hash + Eq + Clone, V: Clone>(cap: usize) -> LfudaCache<K, V> {
let config = LfudaCacheConfig {
capacity: NonZeroUsize::new(cap).unwrap(),
initial_age: 0,
max_size: u64::MAX,
};
LfudaCache::init(config, None)
}
#[test]
fn test_lfuda_basic() {
let mut cache = make_cache(3);
assert_eq!(cache.put("a", 1, 1), None);
assert_eq!(cache.put("b", 2, 1), None);
assert_eq!(cache.put("c", 3, 1), None);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
let evicted = cache.put("d", 4, 1);
assert!(evicted.is_some());
let (evicted_key, evicted_val) = evicted.unwrap()[0];
assert_eq!(evicted_key, "c");
assert_eq!(evicted_val, 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"d"), Some(&4));
assert_eq!(cache.get(&"c"), None); }
#[test]
fn test_lfuda_aging() {
let mut cache = make_cache(2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
for _ in 0..10 {
cache.get(&"a");
}
assert_eq!(cache.global_age(), 0);
let evicted = cache.put("c", 3, 1);
assert!(evicted.is_some());
assert!(cache.global_age() > 0);
cache.put("d", 4, 1);
assert!(cache.len() <= cache.cap().get());
}
#[test]
fn test_lfuda_priority_calculation() {
let mut cache = make_cache(3);
cache.put("a", 1, 1);
assert_eq!(cache.global_age(), 0);
cache.get(&"a");
cache.put("b", 2, 1);
cache.put("c", 3, 1);
let evicted = cache.put("d", 4, 1);
assert!(evicted.is_some());
assert!(cache.global_age() > 0);
}
#[test]
fn test_lfuda_update_existing() {
let mut cache = make_cache(2);
cache.put("a", 1, 1);
cache.get(&"a");
let old_value = cache.put("a", 10, 1);
assert!(old_value.is_none());
cache.put("b", 2, 1);
cache.put("c", 3, 1);
assert_eq!(cache.get(&"a"), Some(&10));
assert_eq!(cache.get(&"c"), Some(&3));
assert_eq!(cache.get(&"b"), None);
}
#[test]
fn test_lfuda_remove() {
let mut cache = make_cache(3);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
assert_eq!(cache.remove(&"b"), Some(2));
assert_eq!(cache.remove(&"b"), None);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"c"), Some(&3));
assert_eq!(cache.len(), 2);
}
#[test]
fn test_lfuda_clear() {
let mut cache = make_cache(3);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
cache.get(&"a");
cache.put("d", 4, 1);
let age_before_clear = cache.global_age();
assert!(age_before_clear > 0);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert_eq!(cache.global_age(), 0);
cache.put("e", 5, 1);
assert_eq!(cache.get(&"e"), Some(&5));
}
#[test]
fn test_lfuda_get_mut() {
let mut cache = make_cache(2);
cache.put("a", 1, 1);
if let Some(value) = cache.get_mut(&"a") {
*value = 10;
}
assert_eq!(cache.get(&"a"), Some(&10));
}
#[test]
fn test_lfuda_complex_values() {
let mut cache = make_cache(2);
#[derive(Debug, Clone, PartialEq)]
struct ComplexValue {
id: usize,
data: String,
}
cache.put(
"a",
ComplexValue {
id: 1,
data: "a-data".to_string(),
},
1,
);
cache.put(
"b",
ComplexValue {
id: 2,
data: "b-data".to_string(),
},
1,
);
if let Some(value) = cache.get_mut(&"a") {
value.id = 100;
value.data = "a-modified".to_string();
}
let a = cache.get(&"a").unwrap();
assert_eq!(a.id, 100);
assert_eq!(a.data, "a-modified");
}
#[test]
fn test_lfuda_aging_advantage() {
let mut cache = make_cache(2);
cache.put("old", 1, 1);
for _ in 0..100 {
cache.get(&"old");
}
cache.put("temp", 2, 1);
let _evicted = cache.put("new1", 3, 1);
let age_after_first_eviction = cache.global_age();
let _evicted = cache.put("new2", 4, 1);
let age_after_second_eviction = cache.global_age();
assert!(age_after_second_eviction >= age_after_first_eviction);
cache.put("newer", 5, 1);
assert!(cache.len() <= cache.cap().get());
}
#[test]
fn test_miri_stacked_borrows_fix() {
let mut cache = make_cache(10);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
for _ in 0..3 {
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
}
assert_eq!(cache.len(), 3);
if let Some(val) = cache.get_mut(&"a") {
*val += 10;
}
assert_eq!(cache.get(&"a"), Some(&11));
}
#[test]
fn test_lfuda_segment_directly() {
let config = LfudaCacheConfig {
capacity: NonZeroUsize::new(3).unwrap(),
initial_age: 0,
max_size: u64::MAX,
};
let mut segment: LfudaSegment<&str, i32, DefaultHashBuilder> =
LfudaSegment::init(config, DefaultHashBuilder::default());
assert_eq!(segment.len(), 0);
assert!(segment.is_empty());
assert_eq!(segment.cap().get(), 3);
assert_eq!(segment.global_age(), 0);
segment.put("a", 1, 1);
segment.put("b", 2, 1);
assert_eq!(segment.len(), 2);
assert_eq!(segment.get(&"a"), Some(&1));
assert_eq!(segment.get(&"b"), Some(&2));
}
#[test]
fn test_lfuda_concurrent_access() {
extern crate std;
use std::sync::{Arc, Mutex};
use std::thread;
use std::vec::Vec;
let cache = Arc::new(Mutex::new(make_cache::<String, i32>(100)));
let num_threads = 4;
let ops_per_thread = 100;
let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
for t in 0..num_threads {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
for i in 0..ops_per_thread {
let key = std::format!("key_{}_{}", t, i);
let mut guard = cache.lock().unwrap();
guard.put(key.clone(), i, 1);
let _ = guard.get(&key);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let mut guard = cache.lock().unwrap();
assert!(guard.len() <= 100);
guard.clear(); }
#[test]
fn test_lfuda_size_aware_tracking() {
let mut cache = make_cache(10);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), u64::MAX);
cache.put("a", 1, 100);
cache.put("b", 2, 200);
cache.put("c", 3, 150);
assert_eq!(cache.current_size(), 450);
assert_eq!(cache.len(), 3);
cache.clear();
assert_eq!(cache.current_size(), 0);
}
#[test]
fn test_lfuda_init_constructor() {
let config = LfudaCacheConfig {
capacity: NonZeroUsize::new(1000).unwrap(),
initial_age: 0,
max_size: 1024 * 1024,
};
let cache: LfudaCache<String, i32> = LfudaCache::init(config, None);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), 1024 * 1024);
}
#[test]
fn test_lfuda_with_limits_constructor() {
let config = LfudaCacheConfig {
capacity: NonZeroUsize::new(100).unwrap(),
initial_age: 0,
max_size: 1024 * 1024,
};
let cache: LfudaCache<String, String> = LfudaCache::init(config, None);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), 1024 * 1024);
assert_eq!(cache.cap().get(), 100);
}
#[test]
fn test_lfuda_contains_non_promoting() {
let mut cache = make_cache(2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
assert!(cache.contains(&"a"));
assert!(cache.contains(&"b"));
assert!(!cache.contains(&"c"));
cache.get(&"b");
assert!(cache.contains(&"a"));
cache.put("c", 3, 1);
assert!(!cache.contains(&"a")); assert!(cache.contains(&"b")); assert!(cache.contains(&"c")); }
#[test]
fn test_put_returns_none_when_no_eviction() {
let mut cache = make_cache(10);
assert!(cache.put("a", 1, 1).is_none());
assert!(cache.put("b", 2, 1).is_none());
}
#[test]
fn test_put_returns_single_eviction() {
let mut cache = make_cache(2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
let result = cache.put("c", 3, 1);
assert!(result.is_some());
let evicted = result.unwrap();
assert_eq!(evicted.len(), 1);
}
#[test]
fn test_put_replacement_returns_none() {
let mut cache = make_cache(10);
cache.put("a", 1, 1);
let result = cache.put("a", 2, 1);
assert!(result.is_none());
assert_eq!(cache.get(&"a"), Some(&2));
}
#[test]
fn test_put_returns_multiple_evictions_size_based() {
let config = LfudaCacheConfig {
capacity: NonZeroUsize::new(10).unwrap(),
initial_age: 0,
max_size: 100,
};
let mut cache = LfudaCache::init(config, None);
for i in 0..10 {
cache.put(i, i, 10);
}
let result = cache.put(99, 99, 50);
let evicted = result.unwrap();
assert_eq!(evicted.len(), 5);
}
#[test]
fn test_lfuda_remove_cleans_up_empty_priority_lists() {
let mut cache = make_cache(5);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
cache.get(&"a");
cache.get(&"a");
cache.remove(&"b");
cache.remove(&"c");
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(&"a"), Some(&1));
cache.put("d", 4, 1);
cache.put("e", 5, 1);
assert_eq!(cache.len(), 3);
}
#[test]
fn test_lfuda_remove_non_min_priority_cleans_up() {
let mut cache = make_cache(5);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
for _ in 0..5 {
cache.get(&"a");
}
cache.remove(&"a");
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
}
#[test]
fn test_lfuda_update_priority_cleans_up_empty_lists() {
let mut cache = make_cache(5);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.get(&"a");
cache.get(&"b");
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
cache.put("c", 3, 1);
cache.put("d", 4, 1);
cache.put("e", 5, 1);
assert_eq!(cache.len(), 5);
cache.put("f", 6, 1);
assert_eq!(cache.len(), 5);
}
}