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}