Skip to main content

cache_mod/
lfu.rs

1//! Least-Frequently-Used (LFU) cache — BTreeMap-indexed O(log n) eviction.
2
3use core::hash::Hash;
4use std::collections::{BTreeMap, HashMap};
5use std::num::NonZeroUsize;
6use std::sync::Mutex;
7
8use crate::cache::Cache;
9use crate::error::CacheError;
10use crate::util::MutexExt;
11
12/// A bounded, thread-safe LFU cache.
13///
14/// Each entry carries a counter that is incremented on every [`get`](Cache::get)
15/// or [`insert`](Cache::insert) of an already-present key. On overflow, the
16/// entry with the **lowest counter** is evicted; ties are broken in favour of
17/// evicting the **least-recently-accessed** entry.
18///
19/// [`contains_key`](Cache::contains_key) is a query and does not increment
20/// the counter or touch access order, per the [`Cache`] contract.
21///
22/// 0.6.0 implementation: a `HashMap` for value lookup paired with a
23/// `BTreeMap<(count, age), K>` ordered index. Every access and eviction is
24/// O(log n) — the 0.5.x O(n) min-scan is gone. The trade-off is one extra
25/// `K::clone()` per access, paid back many-fold once the cache holds more
26/// than a few dozen entries. The sharded / lock-free lock-strategy upgrade
27/// lands in 0.7.0 without changing this public surface.
28///
29/// # Example
30///
31/// ```
32/// use cache_mod::{Cache, LfuCache};
33///
34/// let cache: LfuCache<&'static str, u32> = LfuCache::new(2).expect("capacity > 0");
35///
36/// cache.insert("a", 1);
37/// cache.insert("b", 2);
38///
39/// // Bump "a"'s frequency above "b"'s.
40/// assert_eq!(cache.get(&"a"), Some(1));
41/// assert_eq!(cache.get(&"a"), Some(1));
42///
43/// // Inserting "c" should evict "b" (lowest counter).
44/// cache.insert("c", 3);
45/// assert_eq!(cache.get(&"b"), None);
46/// assert_eq!(cache.get(&"a"), Some(1));
47/// assert_eq!(cache.get(&"c"), Some(3));
48/// ```
49pub struct LfuCache<K, V> {
50    capacity: NonZeroUsize,
51    inner: Mutex<Inner<K, V>>,
52}
53
54struct Entry<V> {
55    value: V,
56    /// Number of accesses since insertion.
57    count: u64,
58    /// Monotonic access marker. Lower = older.
59    age: u64,
60}
61
62struct Inner<K, V> {
63    map: HashMap<K, Entry<V>>,
64    /// Eviction priority index. Sorted by (count, age) — lowest first, so
65    /// `pop_first` gives the least-frequently-used, breaking ties with
66    /// least-recently-accessed.
67    by_priority: BTreeMap<(u64, u64), K>,
68    /// Monotonic clock used to stamp `Entry::age`. Wraps after 2^64 ops.
69    clock: u64,
70}
71
72impl<K, V> Inner<K, V>
73where
74    K: Eq + Hash + Clone,
75{
76    fn with_capacity(cap: usize) -> Self {
77        Self {
78            map: HashMap::with_capacity(cap),
79            by_priority: BTreeMap::new(),
80            clock: 0,
81        }
82    }
83
84    /// Advance the clock and return the new age value.
85    fn tick(&mut self) -> u64 {
86        self.clock = self.clock.wrapping_add(1);
87        self.clock
88    }
89}
90
91impl<K, V> LfuCache<K, V>
92where
93    K: Eq + Hash + Clone,
94    V: Clone,
95{
96    /// Creates a cache with the given capacity.
97    ///
98    /// Returns [`CacheError::InvalidCapacity`] if `capacity == 0`.
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use cache_mod::LfuCache;
104    ///
105    /// let cache: LfuCache<String, u32> = LfuCache::new(128).expect("capacity > 0");
106    /// ```
107    pub fn new(capacity: usize) -> Result<Self, CacheError> {
108        let cap = NonZeroUsize::new(capacity).ok_or(CacheError::InvalidCapacity)?;
109        Ok(Self::with_capacity(cap))
110    }
111
112    /// Creates a cache with the given non-zero capacity. Infallible.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use std::num::NonZeroUsize;
118    /// use cache_mod::LfuCache;
119    ///
120    /// let cap = NonZeroUsize::new(64).expect("64 != 0");
121    /// let cache: LfuCache<String, u32> = LfuCache::with_capacity(cap);
122    /// ```
123    pub fn with_capacity(capacity: NonZeroUsize) -> Self {
124        let cap = capacity.get();
125        Self {
126            capacity,
127            inner: Mutex::new(Inner::with_capacity(cap)),
128        }
129    }
130}
131
132impl<K, V> Cache<K, V> for LfuCache<K, V>
133where
134    K: Eq + Hash + Clone,
135    V: Clone,
136{
137    fn get(&self, key: &K) -> Option<V> {
138        let mut inner = self.inner.lock_recover();
139        let new_age = inner.tick();
140
141        // Extract old priority so we can update the BTreeMap without
142        // double-borrowing.
143        let (old_priority, new_priority, value) = {
144            let entry = inner.map.get_mut(key)?;
145            let old = (entry.count, entry.age);
146            entry.count = entry.count.saturating_add(1);
147            entry.age = new_age;
148            let new = (entry.count, entry.age);
149            (old, new, entry.value.clone())
150        };
151
152        let _ = inner.by_priority.remove(&old_priority);
153        let _ = inner.by_priority.insert(new_priority, key.clone());
154        Some(value)
155    }
156
157    fn insert(&self, key: K, value: V) -> Option<V> {
158        let mut inner = self.inner.lock_recover();
159        let new_age = inner.tick();
160
161        // Live update path.
162        if let Some(entry) = inner.map.get_mut(&key) {
163            let old_priority = (entry.count, entry.age);
164            entry.count = entry.count.saturating_add(1);
165            entry.age = new_age;
166            let new_priority = (entry.count, entry.age);
167            let old_value = core::mem::replace(&mut entry.value, value);
168            let _ = inner.by_priority.remove(&old_priority);
169            let _ = inner.by_priority.insert(new_priority, key);
170            return Some(old_value);
171        }
172
173        // New key — evict if at capacity.
174        if inner.map.len() >= self.capacity.get() {
175            if let Some((victim_priority, victim_key)) = inner.by_priority.pop_first() {
176                let _ = inner.map.remove(&victim_key);
177                // pop_first already removed the priority entry — nothing
178                // more to do. Suppress unused.
179                let _ = victim_priority;
180            }
181        }
182
183        let entry = Entry {
184            value,
185            count: 1,
186            age: new_age,
187        };
188        let priority = (entry.count, entry.age);
189        let _ = inner.map.insert(key.clone(), entry);
190        let _ = inner.by_priority.insert(priority, key);
191        None
192    }
193
194    fn remove(&self, key: &K) -> Option<V> {
195        let mut inner = self.inner.lock_recover();
196        let entry = inner.map.remove(key)?;
197        let _ = inner.by_priority.remove(&(entry.count, entry.age));
198        Some(entry.value)
199    }
200
201    fn contains_key(&self, key: &K) -> bool {
202        self.inner.lock_recover().map.contains_key(key)
203    }
204
205    fn len(&self) -> usize {
206        self.inner.lock_recover().map.len()
207    }
208
209    fn clear(&self) {
210        let mut inner = self.inner.lock_recover();
211        inner.map.clear();
212        inner.by_priority.clear();
213        inner.clock = 0;
214    }
215
216    fn capacity(&self) -> usize {
217        self.capacity.get()
218    }
219}