cache_rs/lfuda.rs
1//! Least Frequently Used with Dynamic Aging (LFUDA) Cache Implementation.
2//!
3//! The LFUDA cache is an enhancement of the basic LFU algorithm that addresses the
4//! "aging problem" where old frequently-used items can prevent new items from being
5//! cached even if they're no longer actively accessed.
6//!
7//! # Algorithm
8//!
9//! LFUDA works by maintaining a global age value that increases when items are evicted.
10//! Each item's priority is calculated using the formula:
11//!
12//! ```text
13//! priority = access_frequency + age_at_insertion
14//! ```
15//!
16//! When an item is accessed, its frequency is incremented. When an item is evicted,
17//! the global age value is set to the priority of the evicted item. New items are
18//! inserted with the current age value as their initial age, giving them a boost to
19//! compete with older items.
20//!
21//! ## Mathematical Formulation
22//!
23//! For each cache entry i:
24//! - Let F_i be the access frequency of item i
25//! - Let A_i be the age factor when item i was inserted
26//! - Let P_i = F_i + A_i be the priority value
27//! - On eviction, select item j where P_j = min{P_i for all i}
28//! - After eviction, set global_age = P_j
29//!
30//! # Performance Characteristics
31//!
32//! - **Time Complexity**:
33//! - Get: O(1)
34//! - Put: O(1)
35//! - Remove: O(1)
36//!
37//! - **Space Complexity**:
38//! - O(n) where n is the capacity of the cache
39//! - Slightly more overhead than LRU due to frequency and age tracking
40//!
41//! # When to Use
42//!
43//! LFUDA caches are ideal for:
44//! - Long-running caches where popularity of items changes over time
45//! - Workloads where frequency of access is more important than recency
46//! - Environments where protecting against cache pollution from one-time scans is important
47//!
48//! # Thread Safety
49//!
50//! This implementation is not thread-safe. For concurrent access, wrap the cache
51//! with a synchronization primitive such as `Mutex` or `RwLock`.
52
53extern crate alloc;
54
55use crate::config::LfudaCacheConfig;
56use crate::list::{Entry, List};
57use crate::metrics::{CacheMetrics, LfudaCacheMetrics};
58use alloc::boxed::Box;
59use alloc::collections::BTreeMap;
60use alloc::string::String;
61use core::borrow::Borrow;
62use core::hash::{BuildHasher, Hash};
63use core::mem;
64use core::num::NonZeroUsize;
65
66#[cfg(feature = "hashbrown")]
67use hashbrown::hash_map::DefaultHashBuilder;
68#[cfg(feature = "hashbrown")]
69use hashbrown::HashMap;
70
71#[cfg(not(feature = "hashbrown"))]
72use std::collections::hash_map::RandomState as DefaultHashBuilder;
73#[cfg(not(feature = "hashbrown"))]
74use std::collections::HashMap;
75
76/// Metadata for each cache entry in LFUDA
77#[derive(Debug, Clone, Copy)]
78struct EntryMetadata<K, V> {
79 /// Frequency of access for this item
80 frequency: usize,
81 /// Age value when this item was inserted
82 age_at_insertion: usize,
83 /// Pointer to the entry in the frequency list
84 node: *mut Entry<(K, V)>,
85}
86
87/// An implementation of a Least Frequently Used with Dynamic Aging (LFUDA) cache.
88///
89/// LFUDA improves upon LFU by adding an aging mechanism that prevents old frequently-used
90/// items from blocking new items indefinitely. Each item's effective priority is calculated
91/// as (access_frequency + age_at_insertion), where age_at_insertion is the global age
92/// value when the item was first inserted.
93///
94/// When an item is evicted, the global age is set to the evicted item's effective priority,
95/// ensuring that new items start with a competitive priority.
96///
97/// # Examples
98///
99/// ```
100/// use cache_rs::lfuda::LfudaCache;
101/// use core::num::NonZeroUsize;
102///
103/// // Create an LFUDA cache with capacity 3
104/// let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
105///
106/// // Add some items
107/// cache.put("a", 1);
108/// cache.put("b", 2);
109/// cache.put("c", 3);
110///
111/// // Access "a" multiple times to increase its frequency
112/// assert_eq!(cache.get(&"a"), Some(&1));
113/// assert_eq!(cache.get(&"a"), Some(&1));
114///
115/// // Add more items to trigger aging
116/// cache.put("d", 4); // This will evict an item and increase global age
117/// cache.put("e", 5); // New items benefit from the increased age
118/// ```
119#[derive(Debug)]
120pub struct LfudaCache<K, V, S = DefaultHashBuilder> {
121 /// Configuration for the LFUDA cache
122 config: LfudaCacheConfig,
123
124 /// Global age value that increases when items are evicted
125 global_age: usize,
126
127 /// Current minimum effective priority in the cache
128 min_priority: usize,
129
130 /// Map from keys to their metadata
131 map: HashMap<K, EntryMetadata<K, V>, S>,
132
133 /// Map from effective priority to list of items with that priority
134 /// Items within each priority list are ordered by recency (LRU within priority)
135 priority_lists: BTreeMap<usize, List<(K, V)>>,
136
137 /// Metrics tracking for this cache instance
138 metrics: LfudaCacheMetrics,
139}
140
141impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaCache<K, V, S> {
142 /// Creates a new LFUDA cache with the specified capacity and hash builder.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// use cache_rs::lfuda::LfudaCache;
148 /// use core::num::NonZeroUsize;
149 /// use std::collections::hash_map::RandomState;
150 ///
151 /// let cache: LfudaCache<&str, u32, _> = LfudaCache::with_hasher(
152 /// NonZeroUsize::new(10).unwrap(),
153 /// RandomState::new()
154 /// );
155 /// ```
156 pub fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
157 let config = LfudaCacheConfig::new(cap);
158 let map_capacity = config.capacity().get().next_power_of_two();
159 let max_cache_size_bytes = config.capacity().get() as u64 * 128; // Estimate based on capacity
160
161 LfudaCache {
162 config,
163 global_age: config.initial_age(),
164 min_priority: 0,
165 map: HashMap::with_capacity_and_hasher(map_capacity, hash_builder),
166 priority_lists: BTreeMap::new(),
167 metrics: LfudaCacheMetrics::new(max_cache_size_bytes),
168 }
169 }
170
171 /// Returns the maximum number of key-value pairs the cache can hold.
172 pub fn cap(&self) -> NonZeroUsize {
173 self.config.capacity()
174 }
175
176 /// Returns the current number of key-value pairs in the cache.
177 pub fn len(&self) -> usize {
178 self.map.len()
179 }
180
181 /// Returns `true` if the cache contains no key-value pairs.
182 pub fn is_empty(&self) -> bool {
183 self.map.is_empty()
184 }
185
186 /// Returns the current global age value.
187 pub fn global_age(&self) -> usize {
188 self.global_age
189 }
190
191 /// Estimates the size of a key-value pair in bytes for metrics tracking
192 fn estimate_object_size(&self, _key: &K, _value: &V) -> u64 {
193 // Simple estimation: key size + value size + overhead for pointers and metadata
194 mem::size_of::<K>() as u64 + mem::size_of::<V>() as u64 + 64
195 }
196
197 /// Records a cache miss for metrics tracking (to be called by simulation system)
198 pub fn record_miss(&mut self, object_size: u64) {
199 self.metrics.core.record_miss(object_size);
200 }
201
202 /// Updates the priority of an item and moves it to the appropriate priority list.
203 unsafe fn update_priority(&mut self, key: &K) -> *mut Entry<(K, V)>
204 where
205 K: Clone,
206 {
207 let metadata = self.map.get_mut(key).unwrap();
208 let old_priority = metadata.frequency + metadata.age_at_insertion;
209
210 // Increment frequency
211 metadata.frequency += 1;
212 let new_priority = metadata.frequency + metadata.age_at_insertion;
213
214 // If priority hasn't changed, just move to front of the same list
215 if old_priority == new_priority {
216 let node = metadata.node;
217 self.priority_lists
218 .get_mut(&old_priority)
219 .unwrap()
220 .move_to_front(node);
221 return node;
222 }
223
224 // Remove from old priority list
225 let node = metadata.node;
226 let boxed_entry = self
227 .priority_lists
228 .get_mut(&old_priority)
229 .unwrap()
230 .remove(node)
231 .unwrap();
232
233 // If the old priority list is now empty and it was the minimum priority,
234 // update the minimum priority
235 if self.priority_lists.get(&old_priority).unwrap().is_empty()
236 && old_priority == self.min_priority
237 {
238 self.min_priority = new_priority;
239 }
240
241 // Add to new priority list
242 let entry_ptr = Box::into_raw(boxed_entry);
243
244 // Ensure the new priority list exists
245 let capacity = self.config.capacity();
246 self.priority_lists
247 .entry(new_priority)
248 .or_insert_with(|| List::new(capacity));
249
250 // Add to the front of the new priority list (most recently used within that priority)
251 self.priority_lists
252 .get_mut(&new_priority)
253 .unwrap()
254 .attach_from_other_list(entry_ptr);
255
256 // Update the metadata
257 metadata.node = entry_ptr;
258
259 entry_ptr
260 }
261
262 /// Returns a reference to the value corresponding to the key.
263 ///
264 /// The key may be any borrowed form of the cache's key type, but
265 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
266 /// the key type.
267 ///
268 /// Accessing an item increases its frequency count.
269 pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
270 where
271 K: Borrow<Q> + Clone,
272 Q: ?Sized + Hash + Eq,
273 {
274 if let Some(metadata) = self.map.get(key) {
275 let node = metadata.node;
276 unsafe {
277 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our priority list
278 // Get the key from the node to pass to update_priority
279 let (key_ref, value) = (*node).get_value();
280
281 // Record hit with object size estimation
282 let object_size = self.estimate_object_size(key_ref, value);
283 self.metrics.core.record_hit(object_size);
284
285 // Record frequency change
286 let _old_frequency = metadata.frequency;
287 let new_node = self.update_priority(key_ref);
288 let (_, value) = (*new_node).get_value();
289 Some(value)
290 }
291 } else {
292 None
293 }
294 }
295
296 /// Returns a mutable reference to the value corresponding to the key.
297 ///
298 /// The key may be any borrowed form of the cache's key type, but
299 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
300 /// the key type.
301 ///
302 /// Accessing an item increases its frequency count.
303 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
304 where
305 K: Borrow<Q> + Clone,
306 Q: ?Sized + Hash + Eq,
307 {
308 if let Some(metadata) = self.map.get(key) {
309 let node = metadata.node;
310 unsafe {
311 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our priority list
312 // Get the key from the node to pass to update_priority
313 let (key_ref, value) = (*node).get_value();
314
315 // Record hit with object size estimation
316 let object_size = self.estimate_object_size(key_ref, value);
317 self.metrics.core.record_hit(object_size);
318
319 // Record frequency change
320 let old_frequency = metadata.frequency;
321 let new_priority = (old_frequency + 1) + metadata.age_at_insertion;
322 self.metrics.record_frequency_increment(new_priority as u64);
323
324 let new_node = self.update_priority(key_ref);
325 let (_, value) = (*new_node).get_value_mut();
326 Some(value)
327 }
328 } else {
329 None
330 }
331 }
332
333 /// Inserts a key-value pair into the cache.
334 ///
335 /// If the cache already contained this key, the old value is replaced and returned.
336 /// Otherwise, if the cache is at capacity, the item with the lowest effective priority
337 /// is evicted. The global age is updated to the evicted item's effective priority.
338 ///
339 /// New items are inserted with a frequency of 1 and age_at_insertion set to the
340 /// current global_age.
341 pub fn put(&mut self, key: K, value: V) -> Option<(K, V)>
342 where
343 K: Clone,
344 {
345 let object_size = self.estimate_object_size(&key, &value);
346
347 // If key already exists, update it
348 if let Some(metadata) = self.map.get(&key) {
349 let node = metadata.node;
350 let priority = metadata.frequency + metadata.age_at_insertion;
351
352 unsafe {
353 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our priority list
354 let old_entry = self.priority_lists.get_mut(&priority).unwrap().update(
355 node,
356 (key.clone(), value),
357 true,
358 );
359
360 // Record insertion (update)
361 self.metrics.core.record_insertion(object_size);
362
363 return old_entry.0;
364 }
365 }
366
367 let mut evicted = None;
368
369 // Add new item with frequency 1 and current global age
370 let frequency = 1;
371 let age_at_insertion = self.global_age;
372 let priority = frequency + age_at_insertion;
373
374 // If at capacity, evict the item with lowest effective priority
375 if self.len() >= self.config.capacity().get() {
376 // Find the list with minimum priority and evict from the end (LRU within priority)
377 if let Some(min_priority_list) = self.priority_lists.get_mut(&self.min_priority) {
378 if let Some(old_entry) = min_priority_list.remove_last() {
379 unsafe {
380 // SAFETY: old_entry comes from min_priority_list.remove_last(), so it's a valid Box
381 // that we own. Converting to raw pointer is safe for temporary access.
382 let entry_ptr = Box::into_raw(old_entry);
383 let (old_key, old_value) = (*entry_ptr).get_value().clone();
384
385 // Record eviction
386 let evicted_size = self.estimate_object_size(&old_key, &old_value);
387 self.metrics.core.record_eviction(evicted_size);
388
389 // Update global age to the evicted item's effective priority
390 if let Some(evicted_metadata) = self.map.get(&old_key) {
391 let _old_global_age = self.global_age;
392 self.global_age =
393 evicted_metadata.frequency + evicted_metadata.age_at_insertion;
394
395 // Record aging event
396 self.metrics.record_aging_event(self.global_age as u64);
397 }
398
399 // Remove from map
400 self.map.remove(&old_key);
401 evicted = Some((old_key, old_value));
402
403 // SAFETY: Reconstructing Box from the same raw pointer to properly free memory
404 let _ = Box::from_raw(entry_ptr);
405
406 // Update min_priority if the list becomes empty
407 if self
408 .priority_lists
409 .get(&self.min_priority)
410 .unwrap()
411 .is_empty()
412 {
413 // Find the next minimum priority
414 self.min_priority = self
415 .priority_lists
416 .keys()
417 .find(|&&p| {
418 p > self.min_priority
419 && !self.priority_lists.get(&p).unwrap().is_empty()
420 })
421 .copied()
422 .unwrap_or(priority); // Use the new item's priority as fallback
423 }
424 }
425 }
426 }
427 }
428
429 self.min_priority = if self.is_empty() {
430 priority
431 } else {
432 core::cmp::min(self.min_priority, priority)
433 };
434
435 // Ensure priority list exists
436 let capacity = self.config.capacity();
437 self.priority_lists
438 .entry(priority)
439 .or_insert_with(|| List::new(capacity));
440
441 if let Some(node) = self
442 .priority_lists
443 .get_mut(&priority)
444 .unwrap()
445 .add((key.clone(), value))
446 {
447 let metadata = EntryMetadata {
448 frequency,
449 age_at_insertion,
450 node,
451 };
452
453 self.map.insert(key, metadata);
454
455 // Record insertion and frequency/aging metrics
456 self.metrics.core.record_insertion(object_size);
457 self.metrics.record_frequency_increment(priority as u64);
458 if age_at_insertion > 0 {
459 self.metrics.record_aging_benefit(age_at_insertion as u64);
460 }
461 }
462
463 evicted
464 }
465
466 /// Removes a key from the cache, returning the value at the key if the key was previously in the cache.
467 ///
468 /// The key may be any borrowed form of the cache's key type, but
469 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
470 /// the key type.
471 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
472 where
473 K: Borrow<Q>,
474 Q: ?Sized + Hash + Eq,
475 V: Clone,
476 {
477 let metadata = self.map.remove(key)?;
478 let priority = metadata.frequency + metadata.age_at_insertion;
479
480 unsafe {
481 // SAFETY: metadata.node comes from our map and was just removed, so priority_lists.remove is safe
482 let boxed_entry = self
483 .priority_lists
484 .get_mut(&priority)?
485 .remove(metadata.node)?;
486 // SAFETY: boxed_entry is a valid Box we own, converting to raw pointer for temporary access
487 let entry_ptr = Box::into_raw(boxed_entry);
488 let value = (*entry_ptr).get_value().1.clone();
489 // SAFETY: Reconstructing Box from the same raw pointer to properly free memory
490 let _ = Box::from_raw(entry_ptr);
491
492 // Update min_priority if necessary
493 if self.priority_lists.get(&priority).unwrap().is_empty()
494 && priority == self.min_priority
495 {
496 // Find the next minimum priority
497 self.min_priority = self
498 .priority_lists
499 .keys()
500 .find(|&&p| p > priority && !self.priority_lists.get(&p).unwrap().is_empty())
501 .copied()
502 .unwrap_or(self.global_age);
503 }
504
505 Some(value)
506 }
507 }
508
509 /// Clears the cache, removing all key-value pairs.
510 /// Resets the global age to 0.
511 pub fn clear(&mut self) {
512 self.map.clear();
513 self.priority_lists.clear();
514 self.global_age = 0;
515 self.min_priority = 0;
516 }
517}
518
519impl<K, V, S> CacheMetrics for LfudaCache<K, V, S> {
520 /// Returns all LFUDA cache metrics as key-value pairs in deterministic order
521 ///
522 /// # Returns
523 /// A BTreeMap containing all metrics tracked by this LFUDA cache instance
524 fn metrics(&self) -> BTreeMap<String, f64> {
525 self.metrics.metrics()
526 }
527
528 /// Returns the algorithm name for this cache implementation
529 ///
530 /// # Returns
531 /// "LFUDA" - identifying this as a Least Frequently Used with Dynamic Aging cache
532 fn algorithm_name(&self) -> &'static str {
533 self.metrics.algorithm_name()
534 }
535}
536
537impl<K: Hash + Eq, V> LfudaCache<K, V>
538where
539 V: Clone,
540{
541 /// Creates a new LFUDA cache with the specified capacity.
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// use cache_rs::lfuda::LfudaCache;
547 /// use core::num::NonZeroUsize;
548 ///
549 /// let cache: LfudaCache<&str, u32> = LfudaCache::new(NonZeroUsize::new(10).unwrap());
550 /// ```
551 pub fn new(cap: NonZeroUsize) -> LfudaCache<K, V, DefaultHashBuilder> {
552 let config = LfudaCacheConfig::new(cap);
553 LfudaCache::with_hasher(config.capacity(), DefaultHashBuilder::default())
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 extern crate std;
560 use alloc::string::ToString;
561
562 use super::*;
563 use alloc::string::String;
564
565 #[test]
566 fn test_lfuda_basic() {
567 let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
568
569 // Add items
570 assert_eq!(cache.put("a", 1), None);
571 assert_eq!(cache.put("b", 2), None);
572 assert_eq!(cache.put("c", 3), None);
573
574 // Access "a" multiple times to increase its frequency
575 assert_eq!(cache.get(&"a"), Some(&1));
576 assert_eq!(cache.get(&"a"), Some(&1));
577
578 // Access "b" once
579 assert_eq!(cache.get(&"b"), Some(&2));
580
581 // Add a new item, should evict "c" (lowest effective priority)
582 let evicted = cache.put("d", 4);
583 assert!(evicted.is_some());
584 let (evicted_key, evicted_val) = evicted.unwrap();
585 assert_eq!(evicted_key, "c");
586 assert_eq!(evicted_val, 3);
587
588 // Check remaining items
589 assert_eq!(cache.get(&"a"), Some(&1));
590 assert_eq!(cache.get(&"b"), Some(&2));
591 assert_eq!(cache.get(&"d"), Some(&4));
592 assert_eq!(cache.get(&"c"), None); // evicted
593 }
594
595 #[test]
596 fn test_lfuda_aging() {
597 let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
598
599 // Add items and access them
600 cache.put("a", 1);
601 cache.put("b", 2);
602
603 // Access "a" many times
604 for _ in 0..10 {
605 cache.get(&"a");
606 }
607
608 // Initially, global age should be 0
609 assert_eq!(cache.global_age(), 0);
610
611 // Fill cache and cause eviction
612 let evicted = cache.put("c", 3);
613 assert!(evicted.is_some());
614
615 // Global age should have increased after eviction
616 assert!(cache.global_age() > 0);
617
618 // New items should benefit from the increased global age
619 cache.put("d", 4);
620
621 // The new item should start with competitive priority due to aging
622 assert!(cache.len() <= cache.cap().get());
623 }
624
625 #[test]
626 fn test_lfuda_priority_calculation() {
627 let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
628
629 cache.put("a", 1);
630 assert_eq!(cache.global_age(), 0);
631
632 // Access "a" to increase its frequency
633 cache.get(&"a");
634
635 // Add more items
636 cache.put("b", 2);
637 cache.put("c", 3);
638
639 // Force eviction to increase global age
640 let evicted = cache.put("d", 4);
641 assert!(evicted.is_some());
642
643 // Global age should now be greater than 0
644 assert!(cache.global_age() > 0);
645 }
646
647 #[test]
648 fn test_lfuda_update_existing() {
649 let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
650
651 cache.put("a", 1);
652 cache.get(&"a"); // Increase frequency
653
654 // Update existing key
655 let old_value = cache.put("a", 10);
656 assert_eq!(old_value.unwrap().1, 1);
657
658 // Add another item
659 cache.put("b", 2);
660 cache.put("c", 3); // Should evict "b" because "a" has higher effective priority
661
662 assert_eq!(cache.get(&"a"), Some(&10));
663 assert_eq!(cache.get(&"c"), Some(&3));
664 assert_eq!(cache.get(&"b"), None);
665 }
666
667 #[test]
668 fn test_lfuda_remove() {
669 let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
670
671 cache.put("a", 1);
672 cache.put("b", 2);
673 cache.put("c", 3);
674
675 // Remove item
676 assert_eq!(cache.remove(&"b"), Some(2));
677 assert_eq!(cache.remove(&"b"), None);
678
679 // Check remaining items
680 assert_eq!(cache.get(&"a"), Some(&1));
681 assert_eq!(cache.get(&"c"), Some(&3));
682 assert_eq!(cache.len(), 2);
683 }
684
685 #[test]
686 fn test_lfuda_clear() {
687 let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
688
689 cache.put("a", 1);
690 cache.put("b", 2);
691 cache.put("c", 3);
692
693 // Force some aging
694 cache.get(&"a");
695 cache.put("d", 4); // This should increase global_age
696
697 let age_before_clear = cache.global_age();
698 assert!(age_before_clear > 0);
699
700 cache.clear();
701 assert_eq!(cache.len(), 0);
702 assert!(cache.is_empty());
703 assert_eq!(cache.global_age(), 0); // Should reset to 0
704
705 // Should be able to add items after clear
706 cache.put("e", 5);
707 assert_eq!(cache.get(&"e"), Some(&5));
708 }
709
710 #[test]
711 fn test_lfuda_get_mut() {
712 let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
713
714 cache.put("a", 1);
715
716 // Modify value using get_mut
717 if let Some(value) = cache.get_mut(&"a") {
718 *value = 10;
719 }
720
721 assert_eq!(cache.get(&"a"), Some(&10));
722 }
723
724 #[test]
725 fn test_lfuda_complex_values() {
726 let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
727
728 #[derive(Debug, Clone, PartialEq)]
729 struct ComplexValue {
730 id: usize,
731 data: String,
732 }
733
734 // Add complex values
735 cache.put(
736 "a",
737 ComplexValue {
738 id: 1,
739 data: "a-data".to_string(),
740 },
741 );
742
743 cache.put(
744 "b",
745 ComplexValue {
746 id: 2,
747 data: "b-data".to_string(),
748 },
749 );
750
751 // Modify a value using get_mut
752 if let Some(value) = cache.get_mut(&"a") {
753 value.id = 100;
754 value.data = "a-modified".to_string();
755 }
756
757 // Check the modified value
758 let a = cache.get(&"a").unwrap();
759 assert_eq!(a.id, 100);
760 assert_eq!(a.data, "a-modified");
761 }
762
763 #[test]
764 fn test_lfuda_aging_advantage() {
765 let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
766
767 // Add and heavily access an old item
768 cache.put("old", 1);
769 for _ in 0..100 {
770 cache.get(&"old");
771 }
772
773 // Fill cache
774 cache.put("temp", 2);
775
776 // Force eviction to age the cache
777 let _evicted = cache.put("new1", 3);
778 let age_after_first_eviction = cache.global_age();
779
780 // Add more items to further age the cache
781 let _evicted = cache.put("new2", 4);
782 let age_after_second_eviction = cache.global_age();
783
784 // The global age should have increased
785 assert!(age_after_second_eviction >= age_after_first_eviction);
786
787 // Now add a brand new item - it should benefit from the aging
788 cache.put("newer", 5);
789
790 // The newer item should be able to compete despite the old item's high frequency
791 // This demonstrates that aging helps newer items compete
792 assert!(cache.len() <= cache.cap().get());
793 }
794}