1use core::{fmt, hash};
2
3use alloc::vec::Vec;
4use alloy_primitives::map::HashMap;
5
6const LFU_MAX_FREQ: u16 = 255;
8
9#[derive(Debug, Clone, Copy)]
11struct LfuEntryMeta {
12 freq: u16,
14 pos: usize,
16}
17
18#[derive(Debug)]
22pub(crate) struct BucketedLfu<K> {
23 capacity: usize,
24 entries: HashMap<K, LfuEntryMeta>,
26 buckets: Vec<Vec<K>>,
29 min_freq: u16,
31}
32
33impl<K> Default for BucketedLfu<K> {
34 fn default() -> Self {
35 Self::new(0)
36 }
37}
38
39impl<K> BucketedLfu<K> {
40 pub(crate) fn new(capacity: usize) -> Self {
41 Self {
42 capacity,
43 entries: HashMap::default(),
44 buckets: (0..=LFU_MAX_FREQ).map(|_| Vec::new()).collect(),
45 min_freq: 1,
46 }
47 }
48}
49
50impl<K: fmt::Debug + Copy + Eq + hash::Hash> BucketedLfu<K> {
51 pub(crate) fn decay_and_evict(&mut self, capacity: usize) {
56 self.capacity = capacity;
57
58 if self.capacity == 0 {
59 self.entries.clear();
60 for b in &mut self.buckets {
61 b.clear();
62 }
63 self.min_freq = 1;
64 return;
65 }
66
67 while self.entries.len() > self.capacity {
68 self.evict_one();
69 }
70 }
71
72 fn evict_one(&mut self) {
74 if self.entries.is_empty() {
75 self.min_freq = 1;
76 return;
77 }
78
79 while (self.min_freq as usize) < self.buckets.len() &&
81 self.buckets[self.min_freq as usize].is_empty()
82 {
83 self.min_freq += 1;
84 }
85
86 if (self.min_freq as usize) >= self.buckets.len() {
87 self.min_freq = 1;
88 return;
89 }
90
91 if let Some(key) = self.buckets[self.min_freq as usize].pop() {
92 self.entries.remove(&key);
93
94 while (self.min_freq as usize) < self.buckets.len() &&
95 self.buckets[self.min_freq as usize].is_empty()
96 {
97 self.min_freq += 1;
98 }
99
100 if (self.min_freq as usize) >= self.buckets.len() {
101 self.min_freq = 1;
102 }
103 }
104 }
105
106 pub(crate) fn touch(&mut self, key: K) {
108 if self.capacity == 0 {
109 return;
110 }
111
112 if let Some(meta) = self.entries.get(&key).copied() {
113 debug_assert_eq!(self.buckets[meta.freq as usize][meta.pos], key);
114
115 let old_freq = meta.freq as usize;
116 let new_freq = meta.freq.saturating_add(1).min(LFU_MAX_FREQ);
117
118 if new_freq as usize != old_freq {
119 self.buckets[old_freq].swap_remove(meta.pos);
121 if let Some(&moved_key) = self.buckets[old_freq].get(meta.pos) {
122 self.entries.get_mut(&moved_key).expect("moved key must exist").pos = meta.pos;
123 }
124
125 let new_pos = self.buckets[new_freq as usize].len();
127 self.buckets[new_freq as usize].push(key);
128
129 let entry = self.entries.get_mut(&key).expect("key must exist");
130 entry.freq = new_freq;
131 entry.pos = new_pos;
132
133 if self.buckets[old_freq].is_empty() && old_freq == self.min_freq as usize {
135 self.min_freq = new_freq;
136 }
137 }
138 } else {
139 if self.entries.len() >= self.capacity {
141 self.evict_one();
142 }
143
144 let pos = self.buckets[1].len();
146 self.buckets[1].push(key);
147 self.entries.insert(key, LfuEntryMeta { freq: 1, pos });
148 self.min_freq = 1;
149 }
150 }
151
152 #[cfg(any(test, feature = "std"))]
154 pub(crate) fn keys(&self) -> impl Iterator<Item = &K> {
155 self.entries.keys()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use alloc::collections::{BTreeMap, BTreeSet};
163 use proptest::prelude::*;
164
165 #[derive(Clone, Copy, Debug)]
166 enum Op {
167 SetCapacity(usize),
168 Touch(u8),
169 }
170
171 #[derive(Debug, Default)]
172 struct ModelLfu {
173 capacity: usize,
174 entries: BTreeMap<u8, u16>,
175 }
176
177 impl ModelLfu {
178 fn apply(&mut self, op: Op) {
179 match op {
180 Op::SetCapacity(capacity) => self.set_capacity(capacity),
181 Op::Touch(key) => self.touch(key),
182 }
183 }
184
185 fn set_capacity(&mut self, capacity: usize) {
186 self.capacity = capacity;
187
188 if capacity == 0 {
189 self.entries.clear();
190 return;
191 }
192
193 while self.entries.len() > self.capacity {
194 self.evict_one();
195 }
196 }
197
198 fn touch(&mut self, key: u8) {
199 if self.capacity == 0 {
200 return;
201 }
202
203 if let Some(freq) = self.entries.get_mut(&key) {
204 *freq = freq.saturating_add(1).min(LFU_MAX_FREQ);
205 return;
206 }
207
208 if self.entries.len() >= self.capacity {
209 self.evict_one();
210 }
211
212 self.entries.insert(key, 1);
213 }
214
215 fn evict_one(&mut self) {
216 let victim = self
217 .entries
218 .iter()
219 .min_by_key(|(key, freq)| (**freq, **key))
220 .map(|(key, _)| *key)
221 .expect("model eviction requires a live entry");
222 self.entries.remove(&victim);
223 }
224
225 fn has_unique_frequencies(&self) -> bool {
226 let mut freqs = BTreeSet::new();
227 self.entries.values().all(|freq| freqs.insert(*freq))
228 }
229
230 fn snapshot(&self) -> BTreeMap<u8, u16> {
231 self.entries.clone()
232 }
233 }
234
235 fn apply_real(lfu: &mut BucketedLfu<u8>, op: Op) {
236 match op {
237 Op::SetCapacity(capacity) => lfu.decay_and_evict(capacity),
238 Op::Touch(key) => lfu.touch(key),
239 }
240 }
241
242 fn snapshot(lfu: &BucketedLfu<u8>) -> BTreeMap<u8, u16> {
243 lfu.entries.iter().map(|(key, meta)| (*key, meta.freq)).collect()
244 }
245
246 fn assert_valid(lfu: &BucketedLfu<u8>) {
247 let mut seen = BTreeSet::new();
248 let mut actual_min_freq = None;
249
250 for freq in 1..lfu.buckets.len() {
251 for (pos, key) in lfu.buckets[freq].iter().copied().enumerate() {
252 assert!(seen.insert(key), "duplicate key {key} in buckets");
253
254 let meta =
255 lfu.entries.get(&key).unwrap_or_else(|| panic!("missing entry for {key}"));
256 assert_eq!(meta.freq as usize, freq, "wrong frequency for key {key}");
257 assert_eq!(meta.pos, pos, "wrong position for key {key}");
258
259 actual_min_freq.get_or_insert(freq as u16);
260 }
261 }
262
263 assert_eq!(seen.len(), lfu.entries.len(), "bucket/entry count mismatch");
264
265 for (key, meta) in &lfu.entries {
266 assert_ne!(meta.freq, 0, "zero frequency for key {key}");
267 assert!(
268 meta.pos < lfu.buckets[meta.freq as usize].len(),
269 "position out of bounds for key {key}"
270 );
271 assert_eq!(
272 lfu.buckets[meta.freq as usize][meta.pos], *key,
273 "bucket position mismatch for key {key}"
274 );
275 }
276
277 assert_eq!(lfu.min_freq, actual_min_freq.unwrap_or(1), "min_freq mismatch");
278 }
279
280 fn is_safe_op(model: &ModelLfu, op: Op) -> bool {
281 match op {
282 Op::SetCapacity(capacity) => {
283 capacity >= model.entries.len() || model.has_unique_frequencies()
284 }
285 Op::Touch(key) => {
286 if model.capacity == 0 {
287 return true;
288 }
289
290 let is_new_key = !model.entries.contains_key(&key);
291 let would_evict = is_new_key && model.entries.len() >= model.capacity;
292 !would_evict || model.has_unique_frequencies()
293 }
294 }
295 }
296
297 fn sanitize_trace(raw_ops: Vec<Op>) -> Vec<Op> {
298 let mut model = ModelLfu::default();
299 let mut safe_ops = Vec::with_capacity(raw_ops.len());
300
301 for op in raw_ops {
302 if is_safe_op(&model, op) {
303 model.apply(op);
304 safe_ops.push(op);
305 }
306 }
307
308 safe_ops
309 }
310
311 fn raw_trace_strategy() -> impl Strategy<Value = Vec<Op>> {
312 prop::collection::vec(
313 prop_oneof![(0usize..=4).prop_map(Op::SetCapacity), (0u8..=5).prop_map(Op::Touch),],
314 0..256,
315 )
316 .prop_map(sanitize_trace)
317 }
318
319 fn check_trace(ops: &[Op]) {
320 let mut real = BucketedLfu::new(0);
321 let mut model = ModelLfu::default();
322
323 for (step, &op) in ops.iter().enumerate() {
324 apply_real(&mut real, op);
325 model.apply(op);
326
327 assert_valid(&real);
328 assert_eq!(snapshot(&real), model.snapshot(), "mismatch at step {step}: {op:?}");
329 }
330 }
331
332 #[test]
333 fn bucketed_lfu_zero_capacity_touches_are_ignored() {
334 let mut lfu = BucketedLfu::default();
335
336 lfu.touch(1);
337 lfu.touch(2);
338
339 assert_valid(&lfu);
340 assert!(lfu.entries.is_empty());
341 assert_eq!(lfu.min_freq, 1);
342 }
343
344 #[test]
345 fn bucketed_lfu_retouch_does_not_duplicate_keys() {
346 check_trace(&[Op::SetCapacity(2), Op::Touch(1), Op::Touch(1), Op::Touch(1)]);
347 }
348
349 #[test]
350 fn bucketed_lfu_shrink_keeps_hottest_keys() {
351 check_trace(&[
352 Op::SetCapacity(4),
353 Op::Touch(1),
354 Op::Touch(2),
355 Op::Touch(2),
356 Op::Touch(3),
357 Op::Touch(3),
358 Op::Touch(3),
359 Op::Touch(4),
360 Op::Touch(4),
361 Op::Touch(4),
362 Op::Touch(4),
363 Op::SetCapacity(2),
364 ]);
365 }
366
367 #[test]
368 fn bucketed_lfu_touch_saturates_frequency() {
369 let mut lfu = BucketedLfu::new(1);
370
371 lfu.touch(1);
372 for _ in 0..(LFU_MAX_FREQ as usize + 16) {
373 lfu.touch(1);
374 }
375
376 assert_valid(&lfu);
377 assert_eq!(lfu.entries[&1].freq, LFU_MAX_FREQ);
378 assert_eq!(lfu.buckets[LFU_MAX_FREQ as usize], vec![1]);
379 }
380
381 #[test]
382 fn bucketed_lfu_tie_eviction_keeps_new_key_and_one_old_key_is_dropped() {
383 let mut lfu = BucketedLfu::new(2);
384
385 lfu.touch(1);
386 lfu.touch(2);
387 lfu.touch(3);
388
389 assert_valid(&lfu);
390 assert_eq!(lfu.entries.len(), 2);
391 assert!(lfu.entries.contains_key(&3));
392 assert_eq!(
393 [lfu.entries.contains_key(&1), lfu.entries.contains_key(&2)]
394 .into_iter()
395 .filter(|present| *present)
396 .count(),
397 1
398 );
399 }
400
401 proptest! {
402 #![proptest_config(ProptestConfig::with_cases(128))]
403
404 #[test]
405 fn bucketed_lfu_model_safe_proptest_traces(ops in raw_trace_strategy()) {
406 check_trace(&ops);
407 }
408 }
409}