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}