Skip to main content

cache_rs/
lfuda.rs

1//! Least Frequently Used with Dynamic Aging (LFUDA) Cache Implementation
2//!
3//! LFUDA is an enhanced LFU algorithm that addresses the "cache pollution" problem
4//! by incorporating a global age factor. This allows the cache to gradually forget
5//! old access patterns, enabling new popular items to compete fairly against
6//! historically popular but now cold items.
7//!
8//! # How the Algorithm Works
9//!
10//! LFUDA maintains a **global age** that increases whenever an item is evicted.
11//! Each item's priority is the sum of its access frequency and the age at which
12//! it was last accessed. This elegant mechanism ensures that:
13//!
14//! 1. Recently accessed items get a "boost" from the current global age
15//! 2. Old items with high frequency but no recent access gradually become eviction candidates
16//! 3. New items can compete fairly against established items
17//!
18//! ## Mathematical Formulation
19//!
20//! ```text
21//! For each cache entry i:
22//!   - F_i = access frequency of item i
23//!   - A_i = global age when item i was last accessed
24//!   - Priority_i = F_i + A_i
25//!
26//! On eviction:
27//!   - Select item j where Priority_j = min{Priority_i for all i}
28//!   - Set global_age = Priority_j
29//!
30//! On access:
31//!   - Increment F_i
32//!   - Update A_i = global_age (item gets current age)
33//! ```
34//!
35//! ## Data Structure
36//!
37//! ```text
38//! ┌─────────────────────────────────────────────────────────────────────────────┐
39//! │                           LFUDA Cache (global_age=100)                       │
40//! │                                                                              │
41//! │  HashMap<K, *Node>              BTreeMap<Priority, List>                     │
42//! │  ┌──────────────┐              ┌─────────────────────────────────────────┐   │
43//! │  │ "hot" ──────────────────────│ pri=115: [hot]    (freq=5, age=110)     │   │
44//! │  │ "warm" ─────────────────────│ pri=103: [warm]   (freq=3, age=100)     │   │
45//! │  │ "stale" ────────────────────│ pri=60:  [stale]  (freq=50, age=10) ←LFU│   │
46//! │  └──────────────┘              └─────────────────────────────────────────┘   │
47//! │                                        ▲                                     │
48//! │                                        │                                     │
49//! │                                   min_priority=60                            │
50//! │                                                                              │
51//! │  Note: "stale" has high frequency (50) but low age (10), making it the      │
52//! │        eviction candidate despite being historically popular.                │
53//! └─────────────────────────────────────────────────────────────────────────────┘
54//! ```
55//!
56//! ## Operations
57//!
58//! | Operation | Action | Time |
59//! |-----------|--------|------|
60//! | `get(key)` | Increment frequency, update age to global_age | O(log P) |
61//! | `put(key, value)` | Insert with priority=global_age+1, evict min priority | O(log P) |
62//! | `remove(key)` | Remove from priority list | O(log P) |
63//!
64//! ## Aging Example
65//!
66//! ```text
67//! global_age = 0
68//!
69//! put("a", 1)    →  a: freq=1, age=0, priority=1
70//! get("a") x10   →  a: freq=11, age=0, priority=11
71//! put("b", 2)    →  b: freq=1, age=0, priority=1
72//! put("c", 3)    →  c: freq=1, age=0, priority=1
73//!
74//! [Cache full, add "d"]
75//! - Evict min priority (b or c with priority=1)
76//! - Set global_age = 1
77//!
78//! put("d", 4)    →  d: freq=1, age=1, priority=2
79//!
80//! [Many evictions later, global_age = 100]
81//! - "a" still has priority=11 (no recent access)
82//! - New item "e" gets priority=101
83//! - "a" becomes eviction candidate despite high frequency!
84//! ```
85//!
86//! # Dual-Limit Capacity
87//!
88//! This implementation supports two independent limits:
89//!
90//! - **`max_entries`**: Maximum number of items
91//! - **`max_size`**: Maximum total size of content
92//!
93//! Eviction occurs when **either** limit would be exceeded.
94//!
95//! # Performance Characteristics
96//!
97//! | Metric | Value |
98//! |--------|-------|
99//! | Get | O(log P) |
100//! | Put | O(log P) amortized |
101//! | Remove | O(log P) |
102//! | Memory per entry | ~110 bytes overhead + key×2 + value |
103//!
104//! Where P = number of distinct priority values. Since priority = frequency + age,
105//! P can grow with the number of entries. BTreeMap provides O(log P) lookups.
106//!
107//! Slightly higher overhead than LFU due to age tracking per entry.
108//!
109//! # When to Use LFUDA
110//!
111//! **Good for:**
112//! - Long-running caches where item popularity changes over time
113//! - Web caches, CDN caches with evolving content popularity
114//! - Systems that need frequency-based eviction but must adapt to trends
115//! - Preventing "cache pollution" from historically popular but now stale items
116//!
117//! **Not ideal for:**
118//! - Short-lived caches (aging doesn't have time to take effect)
119//! - Static popularity patterns (pure LFU is simpler and equally effective)
120//! - Strictly recency-based workloads (use LRU instead)
121//!
122//! # LFUDA vs LFU
123//!
124//! | Aspect | LFU | LFUDA |
125//! |--------|-----|-------|
126//! | Adapts to popularity changes | No | Yes |
127//! | Memory overhead | Lower | Slightly higher |
128//! | Complexity | Simpler | More complex |
129//! | Best for | Stable patterns | Evolving patterns |
130//!
131//! # Thread Safety
132//!
133//! `LfudaCache` is **not thread-safe**. For concurrent access, either:
134//! - Wrap with `Mutex` or `RwLock`
135//! - Use `ConcurrentLfudaCache` (requires `concurrent` feature)
136//!
137//! # Examples
138//!
139//! ## Basic Usage
140//!
141//! ```
142//! use cache_rs::LfudaCache;
143//! use cache_rs::config::LfudaCacheConfig;
144//! use core::num::NonZeroUsize;
145//!
146//! let config = LfudaCacheConfig {
147//!     capacity: NonZeroUsize::new(3).unwrap(),
148//!     initial_age: 0,
149//!     max_size: u64::MAX,
150//! };
151//! let mut cache = LfudaCache::init(config, None);
152//!
153//! cache.put("a", 1);
154//! cache.put("b", 2);
155//! cache.put("c", 3);
156//!
157//! // Access "a" to increase its priority
158//! assert_eq!(cache.get(&"a"), Some(&1));
159//! assert_eq!(cache.get(&"a"), Some(&1));
160//!
161//! // Add new item - lowest priority item evicted
162//! cache.put("d", 4);
163//!
164//! // "a" survives due to higher priority (frequency + age)
165//! assert_eq!(cache.get(&"a"), Some(&1));
166//! ```
167//!
168//! ## Long-Running Cache with Aging
169//!
170//! ```
171//! use cache_rs::LfudaCache;
172//! use cache_rs::config::LfudaCacheConfig;
173//! use core::num::NonZeroUsize;
174//!
175//! let config = LfudaCacheConfig {
176//!     capacity: NonZeroUsize::new(100).unwrap(),
177//!     initial_age: 0,
178//!     max_size: u64::MAX,
179//! };
180//! let mut cache = LfudaCache::init(config, None);
181//!
182//! // Populate cache with initial data
183//! for i in 0..100 {
184//!     cache.put(i, i * 10);
185//! }
186//!
187//! // Access some items frequently
188//! for _ in 0..50 {
189//!     cache.get(&0);  // Item 0 becomes very popular
190//! }
191//!
192//! // Later: new content arrives, old content ages out
193//! for i in 100..200 {
194//!     cache.put(i, i * 10);  // Each insert may evict old items
195//! }
196//!
197//! // Item 0 may eventually be evicted if not accessed recently,
198//! // despite its historical popularity - this is the power of LFUDA!
199//! ```
200
201extern crate alloc;
202
203use crate::config::LfudaCacheConfig;
204use crate::entry::CacheEntry;
205use crate::list::{List, ListEntry};
206use crate::meta::LfudaMeta;
207use crate::metrics::{CacheMetrics, LfudaCacheMetrics};
208use alloc::boxed::Box;
209use alloc::collections::BTreeMap;
210use alloc::string::String;
211use core::borrow::Borrow;
212use core::hash::{BuildHasher, Hash};
213use core::mem;
214use core::num::NonZeroUsize;
215
216#[cfg(feature = "hashbrown")]
217use hashbrown::DefaultHashBuilder;
218#[cfg(feature = "hashbrown")]
219use hashbrown::HashMap;
220
221#[cfg(not(feature = "hashbrown"))]
222use std::collections::hash_map::RandomState as DefaultHashBuilder;
223#[cfg(not(feature = "hashbrown"))]
224use std::collections::HashMap;
225
226/// Internal LFUDA segment containing the actual cache algorithm.
227///
228/// This is shared between `LfudaCache` (single-threaded) and
229/// `ConcurrentLfudaCache` (multi-threaded). All algorithm logic is
230/// implemented here to avoid code duplication.
231///
232/// Uses `CacheEntry<K, V, LfudaMeta>` for unified entry management with built-in
233/// size tracking, timestamps, and LFUDA-specific metadata (frequency + age_at_insertion).
234///
235/// # Safety
236///
237/// This struct contains raw pointers in the `map` field.
238/// These pointers are always valid as long as:
239/// - The pointer was obtained from a `priority_lists` entry's `add()` call
240/// - The node has not been removed from the list
241/// - The segment has not been dropped
242pub(crate) struct LfudaSegment<K, V, S = DefaultHashBuilder> {
243    /// Configuration for the LFUDA cache (includes capacity and max_size)
244    config: LfudaCacheConfig,
245
246    /// Global age value that increases when items are evicted
247    global_age: u64,
248
249    /// Current minimum effective priority in the cache
250    min_priority: u64,
251
252    /// Map from keys to their node pointer.
253    /// All metadata (frequency, age, size) is stored in CacheEntry.
254    map: HashMap<K, *mut ListEntry<CacheEntry<K, V, LfudaMeta>>, S>,
255
256    /// Map from effective priority to list of items with that priority
257    /// Items within each priority list are ordered by recency (LRU within priority)
258    priority_lists: BTreeMap<u64, List<CacheEntry<K, V, LfudaMeta>>>,
259
260    /// Metrics tracking for this cache instance
261    metrics: LfudaCacheMetrics,
262
263    /// Current total size of cached content (sum of entry sizes)
264    current_size: u64,
265}
266
267// SAFETY: LfudaSegment owns all data and raw pointers point only to nodes owned by
268// `priority_lists`. Concurrent access is safe when wrapped in proper synchronization primitives.
269unsafe impl<K: Send, V: Send, S: Send> Send for LfudaSegment<K, V, S> {}
270
271// SAFETY: All mutation requires &mut self; shared references cannot cause data races.
272unsafe impl<K: Send, V: Send, S: Sync> Sync for LfudaSegment<K, V, S> {}
273
274impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaSegment<K, V, S> {
275    /// Creates a new LFUDA segment from a configuration.
276    ///
277    /// This is the **recommended** way to create an LFUDA segment. All configuration
278    /// is specified through the [`LfudaCacheConfig`] struct.
279    ///
280    /// # Arguments
281    ///
282    /// * `config` - Configuration specifying capacity, initial age, and optional size limit
283    /// * `hasher` - Hash builder for the internal HashMap
284    #[allow(dead_code)] // Used by concurrent module when feature is enabled
285    pub(crate) fn init(config: LfudaCacheConfig, hasher: S) -> Self {
286        let map_capacity = config.capacity.get().next_power_of_two();
287        LfudaSegment {
288            config,
289            global_age: config.initial_age as u64,
290            min_priority: 0,
291            map: HashMap::with_capacity_and_hasher(map_capacity, hasher),
292            priority_lists: BTreeMap::new(),
293            metrics: LfudaCacheMetrics::new(config.max_size),
294            current_size: 0,
295        }
296    }
297
298    /// Returns the maximum number of key-value pairs the segment can hold.
299    #[inline]
300    pub(crate) fn cap(&self) -> NonZeroUsize {
301        self.config.capacity
302    }
303
304    /// Returns the current number of key-value pairs in the segment.
305    #[inline]
306    pub(crate) fn len(&self) -> usize {
307        self.map.len()
308    }
309
310    /// Returns `true` if the segment contains no key-value pairs.
311    #[inline]
312    pub(crate) fn is_empty(&self) -> bool {
313        self.map.is_empty()
314    }
315
316    /// Returns the current global age value.
317    #[inline]
318    pub(crate) fn global_age(&self) -> u64 {
319        self.global_age
320    }
321
322    /// Returns the current total size of cached content.
323    #[inline]
324    pub(crate) fn current_size(&self) -> u64 {
325        self.current_size
326    }
327
328    /// Returns the maximum content size the cache can hold.
329    #[inline]
330    pub(crate) fn max_size(&self) -> u64 {
331        self.config.max_size
332    }
333
334    /// Returns a reference to the metrics for this segment.
335    #[inline]
336    pub(crate) fn metrics(&self) -> &LfudaCacheMetrics {
337        &self.metrics
338    }
339
340    /// Estimates the size of a key-value pair in bytes for metrics tracking
341    fn estimate_object_size(&self, _key: &K, _value: &V) -> u64 {
342        mem::size_of::<K>() as u64 + mem::size_of::<V>() as u64 + 64
343    }
344
345    /// Records a cache miss for metrics tracking
346    #[inline]
347    pub(crate) fn record_miss(&mut self, object_size: u64) {
348        self.metrics.core.record_miss(object_size);
349    }
350
351    /// Updates the priority of an item and moves it to the appropriate priority list.
352    ///
353    /// # Safety
354    ///
355    /// The caller must ensure that `node` is a valid pointer to a ListEntry that exists
356    /// in this cache's priority_lists and has not been freed.
357    unsafe fn update_priority_by_node(
358        &mut self,
359        node: *mut ListEntry<CacheEntry<K, V, LfudaMeta>>,
360        old_priority: u64,
361    ) -> *mut ListEntry<CacheEntry<K, V, LfudaMeta>>
362    where
363        K: Clone + Hash + Eq,
364    {
365        // SAFETY: node is guaranteed to be valid by the caller's contract
366        let entry = (*node).get_value();
367        let key_cloned = entry.key.clone();
368
369        // Get current node from map
370        let node = *self.map.get(&key_cloned).unwrap();
371
372        // Calculate new priority after incrementing frequency
373        let meta = (*node).get_value().metadata.as_ref().unwrap();
374        let new_priority = (meta.frequency + 1) + meta.age_at_insertion;
375
376        // If priority hasn't changed, just move to front of the same list
377        if old_priority == new_priority {
378            self.priority_lists
379                .get_mut(&old_priority)
380                .unwrap()
381                .move_to_front(node);
382            return node;
383        }
384
385        // Remove from old priority list
386        let boxed_entry = self
387            .priority_lists
388            .get_mut(&old_priority)
389            .unwrap()
390            .remove(node)
391            .unwrap();
392
393        // If the old priority list is now empty and it was the minimum priority,
394        // update the minimum priority
395        if self.priority_lists.get(&old_priority).unwrap().is_empty()
396            && old_priority == self.min_priority
397        {
398            self.min_priority = new_priority;
399        }
400
401        // Update frequency in the entry's metadata
402        let entry_ptr = Box::into_raw(boxed_entry);
403        if let Some(ref mut meta) = (*entry_ptr).get_value_mut().metadata {
404            meta.frequency += 1;
405        }
406
407        // Ensure the new priority list exists
408        let capacity = self.config.capacity;
409        self.priority_lists
410            .entry(new_priority)
411            .or_insert_with(|| List::new(capacity));
412
413        // Add to the front of the new priority list (most recently used within that priority)
414        self.priority_lists
415            .get_mut(&new_priority)
416            .unwrap()
417            .attach_from_other_list(entry_ptr);
418
419        // Update the map with the new node pointer
420        *self.map.get_mut(&key_cloned).unwrap() = entry_ptr;
421
422        entry_ptr
423    }
424
425    /// Returns a reference to the value corresponding to the key.
426    pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
427    where
428        K: Borrow<Q> + Clone,
429        Q: ?Sized + Hash + Eq,
430    {
431        if let Some(&node) = self.map.get(key) {
432            unsafe {
433                // SAFETY: node comes from our map
434                let entry = (*node).get_value();
435                let meta = entry.metadata.as_ref().unwrap();
436                let old_priority = meta.priority();
437                self.metrics.core.record_hit(entry.size);
438
439                let new_node = self.update_priority_by_node(node, old_priority);
440                let new_entry = (*new_node).get_value();
441                Some(&new_entry.value)
442            }
443        } else {
444            None
445        }
446    }
447
448    /// Returns a mutable reference to the value corresponding to the key.
449    pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
450    where
451        K: Borrow<Q> + Clone,
452        Q: ?Sized + Hash + Eq,
453    {
454        if let Some(&node) = self.map.get(key) {
455            unsafe {
456                // SAFETY: node comes from our map
457                let entry = (*node).get_value();
458                let meta = entry.metadata.as_ref().unwrap();
459                let old_priority = meta.priority();
460                self.metrics.core.record_hit(entry.size);
461
462                let new_priority = (meta.frequency + 1) + meta.age_at_insertion;
463                self.metrics.record_frequency_increment(new_priority);
464
465                let new_node = self.update_priority_by_node(node, old_priority);
466                let new_entry = (*new_node).get_value_mut();
467                Some(&mut new_entry.value)
468            }
469        } else {
470            None
471        }
472    }
473
474    /// Inserts a key-value pair into the segment.
475    pub(crate) fn put(&mut self, key: K, value: V) -> Option<(K, V)>
476    where
477        K: Clone,
478    {
479        let object_size = self.estimate_object_size(&key, &value);
480        self.put_with_size(key, value, object_size)
481    }
482
483    /// Insert a key-value pair with explicit size tracking.
484    pub(crate) fn put_with_size(&mut self, key: K, value: V, size: u64) -> Option<(K, V)>
485    where
486        K: Clone,
487    {
488        // If key already exists, update it
489        if let Some(&node) = self.map.get(&key) {
490            unsafe {
491                // SAFETY: node comes from our map
492                let entry = (*node).get_value();
493                let meta = entry.metadata.as_ref().unwrap();
494                let priority = meta.priority();
495                let old_size = entry.size;
496
497                // Create new CacheEntry with same frequency and age
498                let new_entry = CacheEntry::with_metadata(
499                    key.clone(),
500                    value,
501                    size,
502                    LfudaMeta::new(meta.frequency, meta.age_at_insertion),
503                );
504
505                let old_entry = self
506                    .priority_lists
507                    .get_mut(&priority)
508                    .unwrap()
509                    .update(node, new_entry, true);
510
511                // Update size tracking
512                self.current_size = self.current_size.saturating_sub(old_size);
513                self.current_size += size;
514                self.metrics.core.record_insertion(size);
515
516                // Return old key-value pair
517                return old_entry.0.map(|e| (e.key, e.value));
518            }
519        }
520
521        let mut evicted = None;
522
523        // Add new item with frequency 1 and current global age
524        let frequency: u64 = 1;
525        let age_at_insertion = self.global_age;
526        let priority = frequency + age_at_insertion;
527
528        // Evict while entry count limit OR size limit would be exceeded
529        while self.len() >= self.config.capacity.get()
530            || (self.current_size + size > self.config.max_size && !self.map.is_empty())
531        {
532            if let Some(min_priority_list) = self.priority_lists.get_mut(&self.min_priority) {
533                if let Some(old_entry) = min_priority_list.remove_last() {
534                    unsafe {
535                        let entry_ptr = Box::into_raw(old_entry);
536                        let cache_entry = (*entry_ptr).get_value();
537                        let old_key = cache_entry.key.clone();
538                        let old_value = cache_entry.value.clone();
539                        let evicted_size = cache_entry.size;
540
541                        // Update global age to the evicted item's effective priority
542                        if let Some(ref meta) = cache_entry.metadata {
543                            self.global_age = meta.priority();
544                            self.metrics.record_aging_event(self.global_age);
545                        }
546
547                        self.current_size = self.current_size.saturating_sub(evicted_size);
548                        self.metrics.core.record_eviction(evicted_size);
549                        self.map.remove(&old_key);
550                        evicted = Some((old_key, old_value));
551                        let _ = Box::from_raw(entry_ptr);
552
553                        // Update min_priority if the list becomes empty
554                        if min_priority_list.is_empty() {
555                            self.min_priority = self
556                                .priority_lists
557                                .keys()
558                                .find(|&&p| {
559                                    p > self.min_priority
560                                        && !self
561                                            .priority_lists
562                                            .get(&p)
563                                            .map(|l| l.is_empty())
564                                            .unwrap_or(true)
565                                })
566                                .copied()
567                                .unwrap_or(priority);
568                        }
569                    }
570                } else {
571                    break; // No more items to evict
572                }
573            } else {
574                break; // No priority list at min_priority
575            }
576        }
577
578        self.min_priority = if self.is_empty() {
579            priority
580        } else {
581            core::cmp::min(self.min_priority, priority)
582        };
583
584        // Ensure priority list exists
585        let capacity = self.config.capacity;
586        self.priority_lists
587            .entry(priority)
588            .or_insert_with(|| List::new(capacity));
589
590        // Create CacheEntry with LfudaMeta
591        let cache_entry = CacheEntry::with_metadata(
592            key.clone(),
593            value,
594            size,
595            LfudaMeta::new(frequency, age_at_insertion),
596        );
597
598        if let Some(node) = self
599            .priority_lists
600            .get_mut(&priority)
601            .unwrap()
602            .add(cache_entry)
603        {
604            self.map.insert(key, node);
605            self.current_size += size;
606
607            self.metrics.core.record_insertion(size);
608            self.metrics.record_frequency_increment(priority);
609            if age_at_insertion > 0 {
610                self.metrics.record_aging_benefit(age_at_insertion);
611            }
612        }
613
614        evicted
615    }
616
617    /// Removes a key from the segment, returning the value if the key was present.
618    pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
619    where
620        K: Borrow<Q>,
621        Q: ?Sized + Hash + Eq,
622        V: Clone,
623    {
624        let node = self.map.remove(key)?;
625
626        unsafe {
627            // SAFETY: node comes from our map and was just removed
628            let entry = (*node).get_value();
629            let meta = entry.metadata.as_ref().unwrap();
630            let priority = meta.priority();
631            let removed_size = entry.size;
632            let value = entry.value.clone();
633
634            let boxed_entry = self.priority_lists.get_mut(&priority)?.remove(node)?;
635            let _ = Box::from_raw(Box::into_raw(boxed_entry));
636
637            self.current_size = self.current_size.saturating_sub(removed_size);
638
639            // Update min_priority if necessary
640            if self.priority_lists.get(&priority).unwrap().is_empty()
641                && priority == self.min_priority
642            {
643                self.min_priority = self
644                    .priority_lists
645                    .keys()
646                    .find(|&&p| {
647                        p > priority
648                            && !self
649                                .priority_lists
650                                .get(&p)
651                                .map(|l| l.is_empty())
652                                .unwrap_or(true)
653                    })
654                    .copied()
655                    .unwrap_or(self.global_age);
656            }
657
658            Some(value)
659        }
660    }
661
662    /// Clears the segment, removing all key-value pairs.
663    pub(crate) fn clear(&mut self) {
664        self.map.clear();
665        self.priority_lists.clear();
666        self.global_age = 0;
667        self.min_priority = 0;
668        self.current_size = 0;
669    }
670}
671
672// Implement Debug for LfudaSegment manually since it contains raw pointers
673impl<K, V, S> core::fmt::Debug for LfudaSegment<K, V, S> {
674    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
675        f.debug_struct("LfudaSegment")
676            .field("capacity", &self.config.capacity)
677            .field("len", &self.map.len())
678            .field("global_age", &self.global_age)
679            .field("min_priority", &self.min_priority)
680            .finish()
681    }
682}
683
684/// An implementation of a Least Frequently Used with Dynamic Aging (LFUDA) cache.
685///
686/// LFUDA improves upon LFU by adding an aging mechanism that prevents old frequently-used
687/// items from blocking new items indefinitely. Each item's effective priority is calculated
688/// as (access_frequency + age_at_insertion), where age_at_insertion is the global age
689/// value when the item was first inserted.
690///
691/// When an item is evicted, the global age is set to the evicted item's effective priority,
692/// ensuring that new items start with a competitive priority.
693///
694/// # Examples
695///
696/// ```
697/// use cache_rs::lfuda::LfudaCache;
698/// use cache_rs::config::LfudaCacheConfig;
699/// use core::num::NonZeroUsize;
700///
701/// // Create an LFUDA cache with capacity 3
702/// let config = LfudaCacheConfig {
703///     capacity: NonZeroUsize::new(3).unwrap(),
704///     initial_age: 0,
705///     max_size: u64::MAX,
706/// };
707/// let mut cache = LfudaCache::init(config, None);
708///
709/// // Add some items
710/// cache.put("a", 1);
711/// cache.put("b", 2);
712/// cache.put("c", 3);
713///
714/// // Access "a" multiple times to increase its frequency
715/// assert_eq!(cache.get(&"a"), Some(&1));
716/// assert_eq!(cache.get(&"a"), Some(&1));
717///
718/// // Add more items to trigger aging
719/// cache.put("d", 4); // This will evict an item and increase global age
720/// cache.put("e", 5); // New items benefit from the increased age
721/// ```
722#[derive(Debug)]
723pub struct LfudaCache<K, V, S = DefaultHashBuilder> {
724    segment: LfudaSegment<K, V, S>,
725}
726
727impl<K: Hash + Eq, V: Clone, S: BuildHasher> LfudaCache<K, V, S> {
728    /// Returns the maximum number of key-value pairs the cache can hold.
729    #[inline]
730    pub fn cap(&self) -> NonZeroUsize {
731        self.segment.cap()
732    }
733
734    /// Returns the current number of key-value pairs in the cache.
735    #[inline]
736    pub fn len(&self) -> usize {
737        self.segment.len()
738    }
739
740    /// Returns `true` if the cache contains no key-value pairs.
741    #[inline]
742    pub fn is_empty(&self) -> bool {
743        self.segment.is_empty()
744    }
745
746    /// Returns the current total size of cached content.
747    #[inline]
748    pub fn current_size(&self) -> u64 {
749        self.segment.current_size()
750    }
751
752    /// Returns the maximum content size the cache can hold.
753    #[inline]
754    pub fn max_size(&self) -> u64 {
755        self.segment.max_size()
756    }
757
758    /// Returns the current global age value.
759    #[inline]
760    pub fn global_age(&self) -> u64 {
761        self.segment.global_age()
762    }
763
764    /// Records a cache miss for metrics tracking (to be called by simulation system)
765    #[inline]
766    pub fn record_miss(&mut self, object_size: u64) {
767        self.segment.record_miss(object_size);
768    }
769
770    /// Returns a reference to the value corresponding to the key.
771    ///
772    /// The key may be any borrowed form of the cache's key type, but
773    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
774    /// the key type.
775    ///
776    /// Accessing an item increases its frequency count.
777    #[inline]
778    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
779    where
780        K: Borrow<Q> + Clone,
781        Q: ?Sized + Hash + Eq,
782    {
783        self.segment.get(key)
784    }
785
786    /// Returns a mutable reference to the value corresponding to the key.
787    ///
788    /// The key may be any borrowed form of the cache's key type, but
789    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
790    /// the key type.
791    ///
792    /// Accessing an item increases its frequency count.
793    #[inline]
794    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
795    where
796        K: Borrow<Q> + Clone,
797        Q: ?Sized + Hash + Eq,
798    {
799        self.segment.get_mut(key)
800    }
801
802    /// Inserts a key-value pair into the cache.
803    ///
804    /// If the cache already contained this key, the old value is replaced and returned.
805    /// Otherwise, if the cache is at capacity, the item with the lowest effective priority
806    /// is evicted. The global age is updated to the evicted item's effective priority.
807    ///
808    /// New items are inserted with a frequency of 1 and age_at_insertion set to the
809    /// current global_age.
810    #[inline]
811    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)>
812    where
813        K: Clone,
814    {
815        self.segment.put(key, value)
816    }
817
818    /// Insert a key-value pair with explicit size tracking.
819    ///
820    /// The `size` parameter specifies how much of `max_size` this entry consumes.
821    /// Use `size=1` for count-based caches.
822    #[inline]
823    pub fn put_with_size(&mut self, key: K, value: V, size: u64) -> Option<(K, V)>
824    where
825        K: Clone,
826    {
827        self.segment.put_with_size(key, value, size)
828    }
829
830    /// Removes a key from the cache, returning the value at the key if the key was previously in the cache.
831    ///
832    /// The key may be any borrowed form of the cache's key type, but
833    /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
834    /// the key type.
835    #[inline]
836    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
837    where
838        K: Borrow<Q>,
839        Q: ?Sized + Hash + Eq,
840        V: Clone,
841    {
842        self.segment.remove(key)
843    }
844
845    /// Clears the cache, removing all key-value pairs.
846    /// Resets the global age to 0.
847    #[inline]
848    pub fn clear(&mut self) {
849        self.segment.clear()
850    }
851}
852
853impl<K: Hash + Eq, V: Clone, S: BuildHasher> CacheMetrics for LfudaCache<K, V, S> {
854    fn metrics(&self) -> BTreeMap<String, f64> {
855        self.segment.metrics().metrics()
856    }
857
858    fn algorithm_name(&self) -> &'static str {
859        self.segment.metrics().algorithm_name()
860    }
861}
862
863impl<K: Hash + Eq, V> LfudaCache<K, V>
864where
865    V: Clone,
866{
867    /// Creates a new LFUDA cache from a configuration.
868    ///
869    /// This is the **recommended** way to create an LFUDA cache. All configuration
870    /// is specified through the [`LfudaCacheConfig`] struct, which uses a builder
871    /// pattern for optional parameters.
872    ///
873    /// # Arguments
874    ///
875    /// * `config` - Configuration specifying capacity and optional size limit/initial age
876    ///
877    /// # Example
878    ///
879    /// ```
880    /// use cache_rs::LfudaCache;
881    /// use cache_rs::config::LfudaCacheConfig;
882    /// use core::num::NonZeroUsize;
883    ///
884    /// // Simple capacity-only cache
885    /// let config = LfudaCacheConfig {
886    ///     capacity: NonZeroUsize::new(100).unwrap(),
887    ///     initial_age: 0,
888    ///     max_size: u64::MAX,
889    /// };
890    /// let mut cache: LfudaCache<&str, i32> = LfudaCache::init(config, None);
891    /// cache.put("key", 42);
892    ///
893    /// // Cache with size limit
894    /// let config = LfudaCacheConfig {
895    ///     capacity: NonZeroUsize::new(1000).unwrap(),
896    ///     initial_age: 100,
897    ///     max_size: 10 * 1024 * 1024,  // 10MB
898    /// };
899    /// let cache: LfudaCache<String, Vec<u8>> = LfudaCache::init(config, None);
900    /// ```
901    pub fn init(
902        config: LfudaCacheConfig,
903        hasher: Option<DefaultHashBuilder>,
904    ) -> LfudaCache<K, V, DefaultHashBuilder> {
905        LfudaCache {
906            segment: LfudaSegment::init(config, hasher.unwrap_or_default()),
907        }
908    }
909}
910
911#[cfg(test)]
912mod tests {
913    extern crate std;
914    use alloc::string::ToString;
915
916    use super::*;
917    use crate::config::LfudaCacheConfig;
918    use alloc::string::String;
919
920    /// Helper to create an LfudaCache with the given capacity
921    fn make_cache<K: Hash + Eq + Clone, V: Clone>(cap: usize) -> LfudaCache<K, V> {
922        let config = LfudaCacheConfig {
923            capacity: NonZeroUsize::new(cap).unwrap(),
924            initial_age: 0,
925            max_size: u64::MAX,
926        };
927        LfudaCache::init(config, None)
928    }
929
930    #[test]
931    fn test_lfuda_basic() {
932        let mut cache = make_cache(3);
933
934        // Add items
935        assert_eq!(cache.put("a", 1), None);
936        assert_eq!(cache.put("b", 2), None);
937        assert_eq!(cache.put("c", 3), None);
938
939        // Access "a" multiple times to increase its frequency
940        assert_eq!(cache.get(&"a"), Some(&1));
941        assert_eq!(cache.get(&"a"), Some(&1));
942
943        // Access "b" once
944        assert_eq!(cache.get(&"b"), Some(&2));
945
946        // Add a new item, should evict "c" (lowest effective priority)
947        let evicted = cache.put("d", 4);
948        assert!(evicted.is_some());
949        let (evicted_key, evicted_val) = evicted.unwrap();
950        assert_eq!(evicted_key, "c");
951        assert_eq!(evicted_val, 3);
952
953        // Check remaining items
954        assert_eq!(cache.get(&"a"), Some(&1));
955        assert_eq!(cache.get(&"b"), Some(&2));
956        assert_eq!(cache.get(&"d"), Some(&4));
957        assert_eq!(cache.get(&"c"), None); // evicted
958    }
959
960    #[test]
961    fn test_lfuda_aging() {
962        let mut cache = make_cache(2);
963
964        // Add items and access them
965        cache.put("a", 1);
966        cache.put("b", 2);
967
968        // Access "a" many times
969        for _ in 0..10 {
970            cache.get(&"a");
971        }
972
973        // Initially, global age should be 0
974        assert_eq!(cache.global_age(), 0);
975
976        // Fill cache and cause eviction
977        let evicted = cache.put("c", 3);
978        assert!(evicted.is_some());
979
980        // Global age should have increased after eviction
981        assert!(cache.global_age() > 0);
982
983        // New items should benefit from the increased global age
984        cache.put("d", 4);
985
986        // The new item should start with competitive priority due to aging
987        assert!(cache.len() <= cache.cap().get());
988    }
989
990    #[test]
991    fn test_lfuda_priority_calculation() {
992        let mut cache = make_cache(3);
993
994        cache.put("a", 1);
995        assert_eq!(cache.global_age(), 0);
996
997        // Access "a" to increase its frequency
998        cache.get(&"a");
999
1000        // Add more items
1001        cache.put("b", 2);
1002        cache.put("c", 3);
1003
1004        // Force eviction to increase global age
1005        let evicted = cache.put("d", 4);
1006        assert!(evicted.is_some());
1007
1008        // Global age should now be greater than 0
1009        assert!(cache.global_age() > 0);
1010    }
1011
1012    #[test]
1013    fn test_lfuda_update_existing() {
1014        let mut cache = make_cache(2);
1015
1016        cache.put("a", 1);
1017        cache.get(&"a"); // Increase frequency
1018
1019        // Update existing key
1020        let old_value = cache.put("a", 10);
1021        assert_eq!(old_value.unwrap().1, 1);
1022
1023        // Add another item
1024        cache.put("b", 2);
1025        cache.put("c", 3); // Should evict "b" because "a" has higher effective priority
1026
1027        assert_eq!(cache.get(&"a"), Some(&10));
1028        assert_eq!(cache.get(&"c"), Some(&3));
1029        assert_eq!(cache.get(&"b"), None);
1030    }
1031
1032    #[test]
1033    fn test_lfuda_remove() {
1034        let mut cache = make_cache(3);
1035
1036        cache.put("a", 1);
1037        cache.put("b", 2);
1038        cache.put("c", 3);
1039
1040        // Remove item
1041        assert_eq!(cache.remove(&"b"), Some(2));
1042        assert_eq!(cache.remove(&"b"), None);
1043
1044        // Check remaining items
1045        assert_eq!(cache.get(&"a"), Some(&1));
1046        assert_eq!(cache.get(&"c"), Some(&3));
1047        assert_eq!(cache.len(), 2);
1048    }
1049
1050    #[test]
1051    fn test_lfuda_clear() {
1052        let mut cache = make_cache(3);
1053
1054        cache.put("a", 1);
1055        cache.put("b", 2);
1056        cache.put("c", 3);
1057
1058        // Force some aging
1059        cache.get(&"a");
1060        cache.put("d", 4); // This should increase global_age
1061
1062        let age_before_clear = cache.global_age();
1063        assert!(age_before_clear > 0);
1064
1065        cache.clear();
1066        assert_eq!(cache.len(), 0);
1067        assert!(cache.is_empty());
1068        assert_eq!(cache.global_age(), 0); // Should reset to 0
1069
1070        // Should be able to add items after clear
1071        cache.put("e", 5);
1072        assert_eq!(cache.get(&"e"), Some(&5));
1073    }
1074
1075    #[test]
1076    fn test_lfuda_get_mut() {
1077        let mut cache = make_cache(2);
1078
1079        cache.put("a", 1);
1080
1081        // Modify value using get_mut
1082        if let Some(value) = cache.get_mut(&"a") {
1083            *value = 10;
1084        }
1085
1086        assert_eq!(cache.get(&"a"), Some(&10));
1087    }
1088
1089    #[test]
1090    fn test_lfuda_complex_values() {
1091        let mut cache = make_cache(2);
1092
1093        #[derive(Debug, Clone, PartialEq)]
1094        struct ComplexValue {
1095            id: usize,
1096            data: String,
1097        }
1098
1099        // Add complex values
1100        cache.put(
1101            "a",
1102            ComplexValue {
1103                id: 1,
1104                data: "a-data".to_string(),
1105            },
1106        );
1107
1108        cache.put(
1109            "b",
1110            ComplexValue {
1111                id: 2,
1112                data: "b-data".to_string(),
1113            },
1114        );
1115
1116        // Modify a value using get_mut
1117        if let Some(value) = cache.get_mut(&"a") {
1118            value.id = 100;
1119            value.data = "a-modified".to_string();
1120        }
1121
1122        // Check the modified value
1123        let a = cache.get(&"a").unwrap();
1124        assert_eq!(a.id, 100);
1125        assert_eq!(a.data, "a-modified");
1126    }
1127
1128    #[test]
1129    fn test_lfuda_aging_advantage() {
1130        let mut cache = make_cache(2);
1131
1132        // Add and heavily access an old item
1133        cache.put("old", 1);
1134        for _ in 0..100 {
1135            cache.get(&"old");
1136        }
1137
1138        // Fill cache
1139        cache.put("temp", 2);
1140
1141        // Force eviction to age the cache
1142        let _evicted = cache.put("new1", 3);
1143        let age_after_first_eviction = cache.global_age();
1144
1145        // Add more items to further age the cache
1146        let _evicted = cache.put("new2", 4);
1147        let age_after_second_eviction = cache.global_age();
1148
1149        // The global age should have increased
1150        assert!(age_after_second_eviction >= age_after_first_eviction);
1151
1152        // Now add a brand new item - it should benefit from the aging
1153        cache.put("newer", 5);
1154
1155        // The newer item should be able to compete despite the old item's high frequency
1156        assert!(cache.len() <= cache.cap().get());
1157    }
1158
1159    /// Test to validate the fix for Stacked Borrows violations detected by Miri.
1160    #[test]
1161    fn test_miri_stacked_borrows_fix() {
1162        let mut cache = make_cache(10);
1163
1164        // Insert some items
1165        cache.put("a", 1);
1166        cache.put("b", 2);
1167        cache.put("c", 3);
1168
1169        // Access items multiple times to trigger priority updates
1170        for _ in 0..3 {
1171            assert_eq!(cache.get(&"a"), Some(&1));
1172            assert_eq!(cache.get(&"b"), Some(&2));
1173            assert_eq!(cache.get(&"c"), Some(&3));
1174        }
1175
1176        assert_eq!(cache.len(), 3);
1177
1178        // Test with get_mut as well
1179        if let Some(val) = cache.get_mut(&"a") {
1180            *val += 10;
1181        }
1182        assert_eq!(cache.get(&"a"), Some(&11));
1183    }
1184
1185    // Test that LfudaSegment works correctly (internal tests)
1186    #[test]
1187    fn test_lfuda_segment_directly() {
1188        let config = LfudaCacheConfig {
1189            capacity: NonZeroUsize::new(3).unwrap(),
1190            initial_age: 0,
1191            max_size: u64::MAX,
1192        };
1193        let mut segment: LfudaSegment<&str, i32, DefaultHashBuilder> =
1194            LfudaSegment::init(config, DefaultHashBuilder::default());
1195
1196        assert_eq!(segment.len(), 0);
1197        assert!(segment.is_empty());
1198        assert_eq!(segment.cap().get(), 3);
1199        assert_eq!(segment.global_age(), 0);
1200
1201        segment.put("a", 1);
1202        segment.put("b", 2);
1203        assert_eq!(segment.len(), 2);
1204
1205        // Access to increase frequency
1206        assert_eq!(segment.get(&"a"), Some(&1));
1207        assert_eq!(segment.get(&"b"), Some(&2));
1208    }
1209
1210    #[test]
1211    fn test_lfuda_concurrent_access() {
1212        extern crate std;
1213        use std::sync::{Arc, Mutex};
1214        use std::thread;
1215        use std::vec::Vec;
1216
1217        let cache = Arc::new(Mutex::new(make_cache::<String, i32>(100)));
1218        let num_threads = 4;
1219        let ops_per_thread = 100;
1220
1221        let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
1222
1223        for t in 0..num_threads {
1224            let cache = Arc::clone(&cache);
1225            handles.push(thread::spawn(move || {
1226                for i in 0..ops_per_thread {
1227                    let key = std::format!("key_{}_{}", t, i);
1228                    let mut guard = cache.lock().unwrap();
1229                    guard.put(key.clone(), i);
1230                    let _ = guard.get(&key);
1231                }
1232            }));
1233        }
1234
1235        for handle in handles {
1236            handle.join().unwrap();
1237        }
1238
1239        let mut guard = cache.lock().unwrap();
1240        assert!(guard.len() <= 100);
1241        guard.clear(); // Clean up for MIRI
1242    }
1243
1244    #[test]
1245    fn test_lfuda_size_aware_tracking() {
1246        let mut cache = make_cache(10);
1247
1248        assert_eq!(cache.current_size(), 0);
1249        assert_eq!(cache.max_size(), u64::MAX);
1250
1251        cache.put_with_size("a", 1, 100);
1252        cache.put_with_size("b", 2, 200);
1253        cache.put_with_size("c", 3, 150);
1254
1255        assert_eq!(cache.current_size(), 450);
1256        assert_eq!(cache.len(), 3);
1257
1258        // Clear should reset size
1259        cache.clear();
1260        assert_eq!(cache.current_size(), 0);
1261    }
1262
1263    #[test]
1264    fn test_lfuda_init_constructor() {
1265        let config = LfudaCacheConfig {
1266            capacity: NonZeroUsize::new(1000).unwrap(),
1267            initial_age: 0,
1268            max_size: 1024 * 1024,
1269        };
1270        let cache: LfudaCache<String, i32> = LfudaCache::init(config, None);
1271
1272        assert_eq!(cache.current_size(), 0);
1273        assert_eq!(cache.max_size(), 1024 * 1024);
1274    }
1275
1276    #[test]
1277    fn test_lfuda_with_limits_constructor() {
1278        let config = LfudaCacheConfig {
1279            capacity: NonZeroUsize::new(100).unwrap(),
1280            initial_age: 0,
1281            max_size: 1024 * 1024,
1282        };
1283        let cache: LfudaCache<String, String> = LfudaCache::init(config, None);
1284
1285        assert_eq!(cache.current_size(), 0);
1286        assert_eq!(cache.max_size(), 1024 * 1024);
1287        assert_eq!(cache.cap().get(), 100);
1288    }
1289}