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/// Internal LFUDA segment containing the actual cache algorithm.
88///
89/// This is shared between `LfudaCache` (single-threaded) and
90/// `ConcurrentLfudaCache` (multi-threaded). All algorithm logic is
91/// implemented here to avoid code duplication.
92///
93/// # Safety
94///
95/// This struct contains raw pointers in the `map` field (via EntryMetadata).
96/// These pointers are always valid as long as:
97/// - The pointer was obtained from a `priority_lists` entry's `add()` call
98/// - The node has not been removed from the list
99/// - The segment has not been dropped
100pub(crate) struct LfudaSegment<K, V, S = DefaultHashBuilder> {
101    /// Configuration for the LFUDA cache
102    config: LfudaCacheConfig,
103
104    /// Global age value that increases when items are evicted
105    global_age: usize,
106
107    /// Current minimum effective priority in the cache
108    min_priority: usize,
109
110    /// Map from keys to their metadata
111    map: HashMap<K, EntryMetadata<K, V>, S>,
112
113    /// Map from effective priority to list of items with that priority
114    /// Items within each priority list are ordered by recency (LRU within priority)
115    priority_lists: BTreeMap<usize, List<(K, V)>>,
116
117    /// Metrics tracking for this cache instance
118    metrics: LfudaCacheMetrics,
119}
120
121// SAFETY: LfudaSegment owns all data and raw pointers point only to nodes owned by
122// `priority_lists`. Concurrent access is safe when wrapped in proper synchronization primitives.
123unsafe impl<K: Send, V: Send, S: Send> Send for LfudaSegment<K, V, S> {}
124
125// SAFETY: All mutation requires &mut self; shared references cannot cause data races.
126unsafe impl<K: Send, V: Send, S: Sync> Sync for LfudaSegment<K, V, S> {}
127
128impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaSegment<K, V, S> {
129    /// Creates a new LFUDA segment with the specified capacity and hash builder.
130    pub(crate) fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
131        let config = LfudaCacheConfig::new(cap);
132        let map_capacity = config.capacity().get().next_power_of_two();
133        let max_cache_size_bytes = config.capacity().get() as u64 * 128;
134
135        LfudaSegment {
136            config,
137            global_age: config.initial_age(),
138            min_priority: 0,
139            map: HashMap::with_capacity_and_hasher(map_capacity, hash_builder),
140            priority_lists: BTreeMap::new(),
141            metrics: LfudaCacheMetrics::new(max_cache_size_bytes),
142        }
143    }
144
145    /// Returns the maximum number of key-value pairs the segment can hold.
146    #[inline]
147    pub(crate) fn cap(&self) -> NonZeroUsize {
148        self.config.capacity()
149    }
150
151    /// Returns the current number of key-value pairs in the segment.
152    #[inline]
153    pub(crate) fn len(&self) -> usize {
154        self.map.len()
155    }
156
157    /// Returns `true` if the segment contains no key-value pairs.
158    #[inline]
159    pub(crate) fn is_empty(&self) -> bool {
160        self.map.is_empty()
161    }
162
163    /// Returns the current global age value.
164    #[inline]
165    pub(crate) fn global_age(&self) -> usize {
166        self.global_age
167    }
168
169    /// Returns a reference to the metrics for this segment.
170    #[inline]
171    pub(crate) fn metrics(&self) -> &LfudaCacheMetrics {
172        &self.metrics
173    }
174
175    /// Estimates the size of a key-value pair in bytes for metrics tracking
176    fn estimate_object_size(&self, _key: &K, _value: &V) -> u64 {
177        mem::size_of::<K>() as u64 + mem::size_of::<V>() as u64 + 64
178    }
179
180    /// Records a cache miss for metrics tracking
181    #[inline]
182    pub(crate) fn record_miss(&mut self, object_size: u64) {
183        self.metrics.core.record_miss(object_size);
184    }
185
186    /// Updates the priority of an item and moves it to the appropriate priority list.
187    ///
188    /// # Safety
189    ///
190    /// The caller must ensure that `node` is a valid pointer to an Entry that exists
191    /// in this cache's priority lists and has not been freed.
192    unsafe fn update_priority_by_node(&mut self, node: *mut Entry<(K, V)>) -> *mut Entry<(K, V)>
193    where
194        K: Clone + Hash + Eq,
195    {
196        // SAFETY: node is guaranteed to be valid by the caller's contract
197        let (key_ref, _) = (*node).get_value();
198        let key_cloned = key_ref.clone();
199
200        let metadata = self.map.get_mut(&key_cloned).unwrap();
201        let old_priority = metadata.frequency + metadata.age_at_insertion;
202
203        // Increment frequency
204        metadata.frequency += 1;
205        let new_priority = metadata.frequency + metadata.age_at_insertion;
206
207        // If priority hasn't changed, just move to front of the same list
208        if old_priority == new_priority {
209            let node = metadata.node;
210            self.priority_lists
211                .get_mut(&old_priority)
212                .unwrap()
213                .move_to_front(node);
214            return node;
215        }
216
217        // Remove from old priority list
218        let node = metadata.node;
219        let boxed_entry = self
220            .priority_lists
221            .get_mut(&old_priority)
222            .unwrap()
223            .remove(node)
224            .unwrap();
225
226        // If the old priority list is now empty and it was the minimum priority,
227        // update the minimum priority
228        if self.priority_lists.get(&old_priority).unwrap().is_empty()
229            && old_priority == self.min_priority
230        {
231            self.min_priority = new_priority;
232        }
233
234        // Add to new priority list
235        let entry_ptr = Box::into_raw(boxed_entry);
236
237        // Ensure the new priority list exists
238        let capacity = self.config.capacity();
239        self.priority_lists
240            .entry(new_priority)
241            .or_insert_with(|| List::new(capacity));
242
243        // Add to the front of the new priority list (most recently used within that priority)
244        self.priority_lists
245            .get_mut(&new_priority)
246            .unwrap()
247            .attach_from_other_list(entry_ptr);
248
249        // Update the metadata
250        metadata.node = entry_ptr;
251
252        entry_ptr
253    }
254
255    /// Returns a reference to the value corresponding to the key.
256    pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
257    where
258        K: Borrow<Q> + Clone,
259        Q: ?Sized + Hash + Eq,
260    {
261        if let Some(metadata) = self.map.get(key) {
262            let node = metadata.node;
263            unsafe {
264                // SAFETY: node comes from our map
265                let (key_ref, value) = (*node).get_value();
266                let object_size = self.estimate_object_size(key_ref, value);
267                self.metrics.core.record_hit(object_size);
268
269                let new_node = self.update_priority_by_node(node);
270                let (_, value) = (*new_node).get_value();
271                Some(value)
272            }
273        } else {
274            None
275        }
276    }
277
278    /// Returns a mutable reference to the value corresponding to the key.
279    pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
280    where
281        K: Borrow<Q> + Clone,
282        Q: ?Sized + Hash + Eq,
283    {
284        if let Some(metadata) = self.map.get(key) {
285            let node = metadata.node;
286            unsafe {
287                // SAFETY: node comes from our map
288                let (key_ref, value) = (*node).get_value();
289                let object_size = self.estimate_object_size(key_ref, value);
290                self.metrics.core.record_hit(object_size);
291
292                let old_frequency = metadata.frequency;
293                let new_priority = (old_frequency + 1) + metadata.age_at_insertion;
294                self.metrics.record_frequency_increment(new_priority as u64);
295
296                let new_node = self.update_priority_by_node(node);
297                let (_, value) = (*new_node).get_value_mut();
298                Some(value)
299            }
300        } else {
301            None
302        }
303    }
304
305    /// Inserts a key-value pair into the segment.
306    pub(crate) fn put(&mut self, key: K, value: V) -> Option<(K, V)>
307    where
308        K: Clone,
309    {
310        let object_size = self.estimate_object_size(&key, &value);
311
312        // If key already exists, update it
313        if let Some(metadata) = self.map.get(&key) {
314            let node = metadata.node;
315            let priority = metadata.frequency + metadata.age_at_insertion;
316
317            unsafe {
318                // SAFETY: node comes from our map
319                let old_entry = self.priority_lists.get_mut(&priority).unwrap().update(
320                    node,
321                    (key.clone(), value),
322                    true,
323                );
324
325                self.metrics.core.record_insertion(object_size);
326                return old_entry.0;
327            }
328        }
329
330        let mut evicted = None;
331
332        // Add new item with frequency 1 and current global age
333        let frequency = 1;
334        let age_at_insertion = self.global_age;
335        let priority = frequency + age_at_insertion;
336
337        // If at capacity, evict the item with lowest effective priority
338        if self.len() >= self.config.capacity().get() {
339            if let Some(min_priority_list) = self.priority_lists.get_mut(&self.min_priority) {
340                if let Some(old_entry) = min_priority_list.remove_last() {
341                    unsafe {
342                        let entry_ptr = Box::into_raw(old_entry);
343                        let (old_key, old_value) = (*entry_ptr).get_value().clone();
344
345                        let evicted_size = self.estimate_object_size(&old_key, &old_value);
346                        self.metrics.core.record_eviction(evicted_size);
347
348                        // Update global age to the evicted item's effective priority
349                        if let Some(evicted_metadata) = self.map.get(&old_key) {
350                            self.global_age =
351                                evicted_metadata.frequency + evicted_metadata.age_at_insertion;
352                            self.metrics.record_aging_event(self.global_age as u64);
353                        }
354
355                        self.map.remove(&old_key);
356                        evicted = Some((old_key, old_value));
357                        let _ = Box::from_raw(entry_ptr);
358
359                        // Update min_priority if the list becomes empty
360                        if self
361                            .priority_lists
362                            .get(&self.min_priority)
363                            .unwrap()
364                            .is_empty()
365                        {
366                            self.min_priority = self
367                                .priority_lists
368                                .keys()
369                                .find(|&&p| {
370                                    p > self.min_priority
371                                        && !self.priority_lists.get(&p).unwrap().is_empty()
372                                })
373                                .copied()
374                                .unwrap_or(priority);
375                        }
376                    }
377                }
378            }
379        }
380
381        self.min_priority = if self.is_empty() {
382            priority
383        } else {
384            core::cmp::min(self.min_priority, priority)
385        };
386
387        // Ensure priority list exists
388        let capacity = self.config.capacity();
389        self.priority_lists
390            .entry(priority)
391            .or_insert_with(|| List::new(capacity));
392
393        if let Some(node) = self
394            .priority_lists
395            .get_mut(&priority)
396            .unwrap()
397            .add((key.clone(), value))
398        {
399            let metadata = EntryMetadata {
400                frequency,
401                age_at_insertion,
402                node,
403            };
404
405            self.map.insert(key, metadata);
406
407            self.metrics.core.record_insertion(object_size);
408            self.metrics.record_frequency_increment(priority as u64);
409            if age_at_insertion > 0 {
410                self.metrics.record_aging_benefit(age_at_insertion as u64);
411            }
412        }
413
414        evicted
415    }
416
417    /// Removes a key from the segment, returning the value if the key was present.
418    pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
419    where
420        K: Borrow<Q>,
421        Q: ?Sized + Hash + Eq,
422        V: Clone,
423    {
424        let metadata = self.map.remove(key)?;
425        let priority = metadata.frequency + metadata.age_at_insertion;
426
427        unsafe {
428            // SAFETY: metadata.node comes from our map and was just removed
429            let boxed_entry = self
430                .priority_lists
431                .get_mut(&priority)?
432                .remove(metadata.node)?;
433            let entry_ptr = Box::into_raw(boxed_entry);
434            let value = (*entry_ptr).get_value().1.clone();
435            let _ = Box::from_raw(entry_ptr);
436
437            // Update min_priority if necessary
438            if self.priority_lists.get(&priority).unwrap().is_empty()
439                && priority == self.min_priority
440            {
441                self.min_priority = self
442                    .priority_lists
443                    .keys()
444                    .find(|&&p| p > priority && !self.priority_lists.get(&p).unwrap().is_empty())
445                    .copied()
446                    .unwrap_or(self.global_age);
447            }
448
449            Some(value)
450        }
451    }
452
453    /// Clears the segment, removing all key-value pairs.
454    pub(crate) fn clear(&mut self) {
455        self.map.clear();
456        self.priority_lists.clear();
457        self.global_age = 0;
458        self.min_priority = 0;
459    }
460}
461
462// Implement Debug for LfudaSegment manually since it contains raw pointers
463impl<K, V, S> core::fmt::Debug for LfudaSegment<K, V, S> {
464    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
465        f.debug_struct("LfudaSegment")
466            .field("capacity", &self.config.capacity())
467            .field("len", &self.map.len())
468            .field("global_age", &self.global_age)
469            .field("min_priority", &self.min_priority)
470            .finish()
471    }
472}
473
474/// An implementation of a Least Frequently Used with Dynamic Aging (LFUDA) cache.
475///
476/// LFUDA improves upon LFU by adding an aging mechanism that prevents old frequently-used
477/// items from blocking new items indefinitely. Each item's effective priority is calculated
478/// as (access_frequency + age_at_insertion), where age_at_insertion is the global age
479/// value when the item was first inserted.
480///
481/// When an item is evicted, the global age is set to the evicted item's effective priority,
482/// ensuring that new items start with a competitive priority.
483///
484/// # Examples
485///
486/// ```
487/// use cache_rs::lfuda::LfudaCache;
488/// use core::num::NonZeroUsize;
489///
490/// // Create an LFUDA cache with capacity 3
491/// let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
492///
493/// // Add some items
494/// cache.put("a", 1);
495/// cache.put("b", 2);
496/// cache.put("c", 3);
497///
498/// // Access "a" multiple times to increase its frequency
499/// assert_eq!(cache.get(&"a"), Some(&1));
500/// assert_eq!(cache.get(&"a"), Some(&1));
501///
502/// // Add more items to trigger aging
503/// cache.put("d", 4); // This will evict an item and increase global age
504/// cache.put("e", 5); // New items benefit from the increased age
505/// ```
506#[derive(Debug)]
507pub struct LfudaCache<K, V, S = DefaultHashBuilder> {
508    segment: LfudaSegment<K, V, S>,
509}
510
511impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaCache<K, V, S> {
512    /// Creates a new LFUDA cache with the specified capacity and hash builder.
513    ///
514    /// # Examples
515    ///
516    /// ```
517    /// use cache_rs::lfuda::LfudaCache;
518    /// use core::num::NonZeroUsize;
519    /// use std::collections::hash_map::RandomState;
520    ///
521    /// let cache: LfudaCache<&str, u32, _> = LfudaCache::with_hasher(
522    ///     NonZeroUsize::new(10).unwrap(),
523    ///     RandomState::new()
524    /// );
525    /// ```
526    pub fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
527        Self {
528            segment: LfudaSegment::with_hasher(cap, hash_builder),
529        }
530    }
531
532    /// Returns the maximum number of key-value pairs the cache can hold.
533    #[inline]
534    pub fn cap(&self) -> NonZeroUsize {
535        self.segment.cap()
536    }
537
538    /// Returns the current number of key-value pairs in the cache.
539    #[inline]
540    pub fn len(&self) -> usize {
541        self.segment.len()
542    }
543
544    /// Returns `true` if the cache contains no key-value pairs.
545    #[inline]
546    pub fn is_empty(&self) -> bool {
547        self.segment.is_empty()
548    }
549
550    /// Returns the current global age value.
551    #[inline]
552    pub fn global_age(&self) -> usize {
553        self.segment.global_age()
554    }
555
556    /// Records a cache miss for metrics tracking (to be called by simulation system)
557    #[inline]
558    pub fn record_miss(&mut self, object_size: u64) {
559        self.segment.record_miss(object_size);
560    }
561
562    /// Returns a reference to the value corresponding to the key.
563    ///
564    /// The key may be any borrowed form of the cache's key type, but
565    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
566    /// the key type.
567    ///
568    /// Accessing an item increases its frequency count.
569    #[inline]
570    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
571    where
572        K: Borrow<Q> + Clone,
573        Q: ?Sized + Hash + Eq,
574    {
575        self.segment.get(key)
576    }
577
578    /// Returns a mutable reference to the value corresponding to the key.
579    ///
580    /// The key may be any borrowed form of the cache's key type, but
581    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
582    /// the key type.
583    ///
584    /// Accessing an item increases its frequency count.
585    #[inline]
586    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
587    where
588        K: Borrow<Q> + Clone,
589        Q: ?Sized + Hash + Eq,
590    {
591        self.segment.get_mut(key)
592    }
593
594    /// Inserts a key-value pair into the cache.
595    ///
596    /// If the cache already contained this key, the old value is replaced and returned.
597    /// Otherwise, if the cache is at capacity, the item with the lowest effective priority
598    /// is evicted. The global age is updated to the evicted item's effective priority.
599    ///
600    /// New items are inserted with a frequency of 1 and age_at_insertion set to the
601    /// current global_age.
602    #[inline]
603    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)>
604    where
605        K: Clone,
606    {
607        self.segment.put(key, value)
608    }
609
610    /// Removes a key from the cache, returning the value at the key if the key was previously in the cache.
611    ///
612    /// The key may be any borrowed form of the cache's key type, but
613    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
614    /// the key type.
615    #[inline]
616    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
617    where
618        K: Borrow<Q>,
619        Q: ?Sized + Hash + Eq,
620        V: Clone,
621    {
622        self.segment.remove(key)
623    }
624
625    /// Clears the cache, removing all key-value pairs.
626    /// Resets the global age to 0.
627    #[inline]
628    pub fn clear(&mut self) {
629        self.segment.clear()
630    }
631}
632
633impl<K: Hash + Eq, V: Clone, S: BuildHasher> CacheMetrics for LfudaCache<K, V, S> {
634    fn metrics(&self) -> BTreeMap<String, f64> {
635        self.segment.metrics().metrics()
636    }
637
638    fn algorithm_name(&self) -> &'static str {
639        self.segment.metrics().algorithm_name()
640    }
641}
642
643impl<K: Hash + Eq, V> LfudaCache<K, V>
644where
645    V: Clone,
646{
647    /// Creates a new LFUDA cache with the specified capacity.
648    ///
649    /// # Examples
650    ///
651    /// ```
652    /// use cache_rs::lfuda::LfudaCache;
653    /// use core::num::NonZeroUsize;
654    ///
655    /// let cache: LfudaCache<&str, u32> = LfudaCache::new(NonZeroUsize::new(10).unwrap());
656    /// ```
657    pub fn new(cap: NonZeroUsize) -> LfudaCache<K, V, DefaultHashBuilder> {
658        let config = LfudaCacheConfig::new(cap);
659        LfudaCache::with_hasher(config.capacity(), DefaultHashBuilder::default())
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    extern crate std;
666    use alloc::string::ToString;
667
668    use super::*;
669    use alloc::string::String;
670
671    #[test]
672    fn test_lfuda_basic() {
673        let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
674
675        // Add items
676        assert_eq!(cache.put("a", 1), None);
677        assert_eq!(cache.put("b", 2), None);
678        assert_eq!(cache.put("c", 3), None);
679
680        // Access "a" multiple times to increase its frequency
681        assert_eq!(cache.get(&"a"), Some(&1));
682        assert_eq!(cache.get(&"a"), Some(&1));
683
684        // Access "b" once
685        assert_eq!(cache.get(&"b"), Some(&2));
686
687        // Add a new item, should evict "c" (lowest effective priority)
688        let evicted = cache.put("d", 4);
689        assert!(evicted.is_some());
690        let (evicted_key, evicted_val) = evicted.unwrap();
691        assert_eq!(evicted_key, "c");
692        assert_eq!(evicted_val, 3);
693
694        // Check remaining items
695        assert_eq!(cache.get(&"a"), Some(&1));
696        assert_eq!(cache.get(&"b"), Some(&2));
697        assert_eq!(cache.get(&"d"), Some(&4));
698        assert_eq!(cache.get(&"c"), None); // evicted
699    }
700
701    #[test]
702    fn test_lfuda_aging() {
703        let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
704
705        // Add items and access them
706        cache.put("a", 1);
707        cache.put("b", 2);
708
709        // Access "a" many times
710        for _ in 0..10 {
711            cache.get(&"a");
712        }
713
714        // Initially, global age should be 0
715        assert_eq!(cache.global_age(), 0);
716
717        // Fill cache and cause eviction
718        let evicted = cache.put("c", 3);
719        assert!(evicted.is_some());
720
721        // Global age should have increased after eviction
722        assert!(cache.global_age() > 0);
723
724        // New items should benefit from the increased global age
725        cache.put("d", 4);
726
727        // The new item should start with competitive priority due to aging
728        assert!(cache.len() <= cache.cap().get());
729    }
730
731    #[test]
732    fn test_lfuda_priority_calculation() {
733        let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
734
735        cache.put("a", 1);
736        assert_eq!(cache.global_age(), 0);
737
738        // Access "a" to increase its frequency
739        cache.get(&"a");
740
741        // Add more items
742        cache.put("b", 2);
743        cache.put("c", 3);
744
745        // Force eviction to increase global age
746        let evicted = cache.put("d", 4);
747        assert!(evicted.is_some());
748
749        // Global age should now be greater than 0
750        assert!(cache.global_age() > 0);
751    }
752
753    #[test]
754    fn test_lfuda_update_existing() {
755        let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
756
757        cache.put("a", 1);
758        cache.get(&"a"); // Increase frequency
759
760        // Update existing key
761        let old_value = cache.put("a", 10);
762        assert_eq!(old_value.unwrap().1, 1);
763
764        // Add another item
765        cache.put("b", 2);
766        cache.put("c", 3); // Should evict "b" because "a" has higher effective priority
767
768        assert_eq!(cache.get(&"a"), Some(&10));
769        assert_eq!(cache.get(&"c"), Some(&3));
770        assert_eq!(cache.get(&"b"), None);
771    }
772
773    #[test]
774    fn test_lfuda_remove() {
775        let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
776
777        cache.put("a", 1);
778        cache.put("b", 2);
779        cache.put("c", 3);
780
781        // Remove item
782        assert_eq!(cache.remove(&"b"), Some(2));
783        assert_eq!(cache.remove(&"b"), None);
784
785        // Check remaining items
786        assert_eq!(cache.get(&"a"), Some(&1));
787        assert_eq!(cache.get(&"c"), Some(&3));
788        assert_eq!(cache.len(), 2);
789    }
790
791    #[test]
792    fn test_lfuda_clear() {
793        let mut cache = LfudaCache::new(NonZeroUsize::new(3).unwrap());
794
795        cache.put("a", 1);
796        cache.put("b", 2);
797        cache.put("c", 3);
798
799        // Force some aging
800        cache.get(&"a");
801        cache.put("d", 4); // This should increase global_age
802
803        let age_before_clear = cache.global_age();
804        assert!(age_before_clear > 0);
805
806        cache.clear();
807        assert_eq!(cache.len(), 0);
808        assert!(cache.is_empty());
809        assert_eq!(cache.global_age(), 0); // Should reset to 0
810
811        // Should be able to add items after clear
812        cache.put("e", 5);
813        assert_eq!(cache.get(&"e"), Some(&5));
814    }
815
816    #[test]
817    fn test_lfuda_get_mut() {
818        let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
819
820        cache.put("a", 1);
821
822        // Modify value using get_mut
823        if let Some(value) = cache.get_mut(&"a") {
824            *value = 10;
825        }
826
827        assert_eq!(cache.get(&"a"), Some(&10));
828    }
829
830    #[test]
831    fn test_lfuda_complex_values() {
832        let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
833
834        #[derive(Debug, Clone, PartialEq)]
835        struct ComplexValue {
836            id: usize,
837            data: String,
838        }
839
840        // Add complex values
841        cache.put(
842            "a",
843            ComplexValue {
844                id: 1,
845                data: "a-data".to_string(),
846            },
847        );
848
849        cache.put(
850            "b",
851            ComplexValue {
852                id: 2,
853                data: "b-data".to_string(),
854            },
855        );
856
857        // Modify a value using get_mut
858        if let Some(value) = cache.get_mut(&"a") {
859            value.id = 100;
860            value.data = "a-modified".to_string();
861        }
862
863        // Check the modified value
864        let a = cache.get(&"a").unwrap();
865        assert_eq!(a.id, 100);
866        assert_eq!(a.data, "a-modified");
867    }
868
869    #[test]
870    fn test_lfuda_aging_advantage() {
871        let mut cache = LfudaCache::new(NonZeroUsize::new(2).unwrap());
872
873        // Add and heavily access an old item
874        cache.put("old", 1);
875        for _ in 0..100 {
876            cache.get(&"old");
877        }
878
879        // Fill cache
880        cache.put("temp", 2);
881
882        // Force eviction to age the cache
883        let _evicted = cache.put("new1", 3);
884        let age_after_first_eviction = cache.global_age();
885
886        // Add more items to further age the cache
887        let _evicted = cache.put("new2", 4);
888        let age_after_second_eviction = cache.global_age();
889
890        // The global age should have increased
891        assert!(age_after_second_eviction >= age_after_first_eviction);
892
893        // Now add a brand new item - it should benefit from the aging
894        cache.put("newer", 5);
895
896        // The newer item should be able to compete despite the old item's high frequency
897        assert!(cache.len() <= cache.cap().get());
898    }
899
900    /// Test to validate the fix for Stacked Borrows violations detected by Miri.
901    #[test]
902    fn test_miri_stacked_borrows_fix() {
903        let mut cache = LfudaCache::new(NonZeroUsize::new(10).unwrap());
904
905        // Insert some items
906        cache.put("a", 1);
907        cache.put("b", 2);
908        cache.put("c", 3);
909
910        // Access items multiple times to trigger priority updates
911        for _ in 0..3 {
912            assert_eq!(cache.get(&"a"), Some(&1));
913            assert_eq!(cache.get(&"b"), Some(&2));
914            assert_eq!(cache.get(&"c"), Some(&3));
915        }
916
917        assert_eq!(cache.len(), 3);
918
919        // Test with get_mut as well
920        if let Some(val) = cache.get_mut(&"a") {
921            *val += 10;
922        }
923        assert_eq!(cache.get(&"a"), Some(&11));
924    }
925
926    // Test that LfudaSegment works correctly (internal tests)
927    #[test]
928    fn test_lfuda_segment_directly() {
929        let mut segment: LfudaSegment<&str, i32, DefaultHashBuilder> =
930            LfudaSegment::with_hasher(NonZeroUsize::new(3).unwrap(), DefaultHashBuilder::default());
931
932        assert_eq!(segment.len(), 0);
933        assert!(segment.is_empty());
934        assert_eq!(segment.cap().get(), 3);
935        assert_eq!(segment.global_age(), 0);
936
937        segment.put("a", 1);
938        segment.put("b", 2);
939        assert_eq!(segment.len(), 2);
940
941        // Access to increase frequency
942        assert_eq!(segment.get(&"a"), Some(&1));
943        assert_eq!(segment.get(&"b"), Some(&2));
944    }
945
946    #[test]
947    fn test_lfuda_concurrent_access() {
948        extern crate std;
949        use std::sync::{Arc, Mutex};
950        use std::thread;
951        use std::vec::Vec;
952
953        let cache = Arc::new(Mutex::new(LfudaCache::new(NonZeroUsize::new(100).unwrap())));
954        let num_threads = 4;
955        let ops_per_thread = 100;
956
957        let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
958
959        for t in 0..num_threads {
960            let cache = Arc::clone(&cache);
961            handles.push(thread::spawn(move || {
962                for i in 0..ops_per_thread {
963                    let key = std::format!("key_{}_{}", t, i);
964                    let mut guard = cache.lock().unwrap();
965                    guard.put(key.clone(), i);
966                    let _ = guard.get(&key);
967                }
968            }));
969        }
970
971        for handle in handles {
972            handle.join().unwrap();
973        }
974
975        let mut guard = cache.lock().unwrap();
976        assert!(guard.len() <= 100);
977        guard.clear(); // Clean up for MIRI
978    }
979}