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}