Skip to main content

cranpose_render_common/
bounded_lru_cache.rs

1use std::collections::HashMap;
2use std::hash::Hash;
3use std::num::NonZeroUsize;
4
5struct CacheEntry<V> {
6    value: V,
7    last_used: u64,
8}
9
10/// Small bounded LRU cache used by renderer hot-path caches.
11///
12/// Hits update recency in place, so the common path is a single hash lookup.
13/// Eviction scans the bounded table and happens only when inserting into a full
14/// cache.
15pub struct BoundedLruCache<K, V> {
16    entries: HashMap<K, CacheEntry<V>>,
17    cap: NonZeroUsize,
18    clock: u64,
19}
20
21impl<K, V> BoundedLruCache<K, V>
22where
23    K: Clone + Eq + Hash,
24{
25    pub fn new(cap: NonZeroUsize) -> Self {
26        Self {
27            entries: HashMap::with_capacity(cap.get()),
28            cap,
29            clock: 0,
30        }
31    }
32
33    pub fn with_capacity_at_least_one(cap: usize) -> Self {
34        let cap = NonZeroUsize::new(cap).unwrap_or(NonZeroUsize::MIN);
35        Self::new(cap)
36    }
37
38    pub fn len(&self) -> usize {
39        self.entries.len()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.entries.is_empty()
44    }
45
46    pub fn cap(&self) -> NonZeroUsize {
47        self.cap
48    }
49
50    pub fn contains(&self, key: &K) -> bool {
51        self.entries.contains_key(key)
52    }
53
54    pub fn get(&mut self, key: &K) -> Option<&V> {
55        let tick = self.next_tick();
56        let entry = self.entries.get_mut(key)?;
57        entry.last_used = tick;
58        Some(&entry.value)
59    }
60
61    pub fn peek(&self, key: &K) -> Option<&V> {
62        self.entries.get(key).map(|entry| &entry.value)
63    }
64
65    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
66        let tick = self.next_tick();
67        let entry = self.entries.get_mut(key)?;
68        entry.last_used = tick;
69        Some(&mut entry.value)
70    }
71
72    pub fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
73        let tick = self.next_tick();
74        if let Some(entry) = self.entries.get_mut(&key) {
75            entry.last_used = tick;
76            let old_value = std::mem::replace(&mut entry.value, value);
77            return Some((key, old_value));
78        }
79
80        let evicted = if self.entries.len() == self.cap.get() {
81            self.pop_lru()
82        } else {
83            None
84        };
85        self.entries.insert(
86            key,
87            CacheEntry {
88                value,
89                last_used: tick,
90            },
91        );
92        evicted
93    }
94
95    pub fn put(&mut self, key: K, value: V) -> Option<V> {
96        self.push(key, value).map(|(_, value)| value)
97    }
98
99    pub fn pop_lru(&mut self) -> Option<(K, V)> {
100        let key = self
101            .entries
102            .iter()
103            .min_by_key(|(_, entry)| entry.last_used)
104            .map(|(key, _)| key.clone())?;
105        self.entries
106            .remove_entry(&key)
107            .map(|(key, entry)| (key, entry.value))
108    }
109
110    pub fn pop(&mut self, key: &K) -> Option<V> {
111        self.entries.remove(key).map(|entry| entry.value)
112    }
113
114    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
115        let mut entries = self.entries.iter().collect::<Vec<_>>();
116        entries.sort_by_key(|(_, entry)| std::cmp::Reverse(entry.last_used));
117        entries.into_iter().map(|(key, entry)| (key, &entry.value))
118    }
119
120    fn next_tick(&mut self) -> u64 {
121        self.clock = self.clock.saturating_add(1);
122        self.clock
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::BoundedLruCache;
129
130    fn cache<K, V>(cap: usize) -> BoundedLruCache<K, V>
131    where
132        K: Clone + Eq + std::hash::Hash,
133    {
134        BoundedLruCache::with_capacity_at_least_one(cap)
135    }
136
137    #[test]
138    fn clamped_constructor_uses_minimum_nonzero_capacity() {
139        let mut cache = BoundedLruCache::with_capacity_at_least_one(0);
140        assert_eq!(cache.cap().get(), 1);
141        assert_eq!(cache.push("a", 1), None);
142        assert_eq!(cache.push("b", 2), Some(("a", 1)));
143        assert_eq!(cache.get(&"b"), Some(&2));
144    }
145
146    #[test]
147    fn get_promotes_entry_and_push_evicts_lru() {
148        let mut cache = cache(2);
149        assert_eq!(cache.push("a", 1), None);
150        assert_eq!(cache.push("b", 2), None);
151
152        assert_eq!(cache.get(&"a"), Some(&1));
153        assert_eq!(cache.push("c", 3), Some(("b", 2)));
154
155        assert!(cache.contains(&"a"));
156        assert!(cache.contains(&"c"));
157        assert!(!cache.contains(&"b"));
158    }
159
160    #[test]
161    fn push_existing_replaces_value_and_keeps_capacity() {
162        let mut cache = cache(2);
163        cache.push("a", 1);
164        cache.push("b", 2);
165
166        assert_eq!(cache.push("a", 3), Some(("a", 1)));
167        assert_eq!(cache.len(), 2);
168        assert_eq!(cache.get(&"a"), Some(&3));
169    }
170
171    #[test]
172    fn pop_removes_requested_entry_and_preserves_lru_order() {
173        let mut cache = cache(3);
174        cache.push("a", 1);
175        cache.push("b", 2);
176        cache.push("c", 3);
177
178        assert_eq!(cache.pop(&"b"), Some(2));
179        assert_eq!(cache.get(&"a"), Some(&1));
180        assert_eq!(cache.pop_lru(), Some(("c", 3)));
181        assert_eq!(cache.len(), 1);
182    }
183
184    #[test]
185    fn peek_reads_without_promoting_entry() {
186        let mut cache = cache(2);
187        cache.push("a", 1);
188        cache.push("b", 2);
189
190        assert_eq!(cache.peek(&"a"), Some(&1));
191        assert_eq!(cache.push("c", 3), Some(("a", 1)));
192    }
193
194    #[test]
195    fn iter_reports_mru_to_lru_entries() {
196        let mut cache = cache(3);
197        cache.push("a", 1);
198        cache.push("b", 2);
199        cache.get(&"a");
200
201        let entries: Vec<_> = cache.iter().map(|(key, value)| (*key, *value)).collect();
202        assert_eq!(entries, vec![("a", 1), ("b", 2)]);
203    }
204}