Skip to main content

cache_rs/
lfu.rs

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