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}