Skip to main content

reth_trie_sparse/
lfu.rs

1use core::{fmt, hash};
2
3use alloc::vec::Vec;
4use alloy_primitives::map::HashMap;
5
6/// Maximum frequency value. Frequencies are capped here to bound bucket storage.
7const LFU_MAX_FREQ: u16 = 255;
8
9/// Per-entry metadata stored in the LFU lookup map.
10#[derive(Debug, Clone, Copy)]
11struct LfuEntryMeta {
12    /// Current frequency (1..=`LFU_MAX_FREQ`).
13    freq: u16,
14    /// Index of this key within `buckets[freq]`.
15    pos: usize,
16}
17
18/// Bucketed LFU cache with O(1) eviction and O(1) touch operations.
19///
20/// Generic over the key type `K`.
21#[derive(Debug)]
22pub(crate) struct BucketedLfu<K> {
23    capacity: usize,
24    /// Maps each key to its frequency and bucket position.
25    entries: HashMap<K, LfuEntryMeta>,
26    /// `buckets[f]` holds all keys currently at frequency `f`.
27    /// Index 0 is unused; valid frequencies are 1..=`LFU_MAX_FREQ`.
28    buckets: Vec<Vec<K>>,
29    /// Smallest non-empty frequency bucket.
30    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    /// Updates the capacity and evicts the least-frequently-used entries that exceed it.
52    ///
53    /// Entries accumulate via [`Self::touch`] over time. This method trims the cache
54    /// so that only the `capacity` hottest entries are retained.
55    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    /// Evicts a single entry from the lowest-frequency bucket.
73    fn evict_one(&mut self) {
74        if self.entries.is_empty() {
75            self.min_freq = 1;
76            return;
77        }
78
79        // Advance min_freq to the next non-empty bucket.
80        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    /// Records a key touch. O(1) amortized.
107    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                // Remove from old bucket via swap_remove and fix the swapped element's pos.
120                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                // Insert into new bucket.
126                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                // Update min_freq if old bucket is now empty.
134                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            // Evict if at capacity before inserting.
140            if self.entries.len() >= self.capacity {
141                self.evict_one();
142            }
143
144            // New entry at frequency 1.
145            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    /// Returns an iterator over all retained keys.
153    #[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}