Skip to main content

cranpose_render_common/
bounded_lru_cache.rs

1use std::{borrow::Borrow, hash::Hash, num::NonZeroUsize};
2
3use cranpose_core::collections::map::HashMap;
4
5struct CacheSlot<K, V> {
6    key: K,
7    value: V,
8    newer: Option<usize>,
9    older: Option<usize>,
10}
11
12/// Small bounded LRU cache used by renderer hot-path caches.
13///
14/// Hits update recency in place, so the common path is a single hash lookup.
15/// Eviction unlinks the oldest entry, which costs the same whether the cache
16/// holds ten entries or ten thousand.
17///
18/// The recency order is a linked list rather than a timestamp per entry
19/// because a timestamp makes eviction a scan for the minimum. These caches are
20/// large -- thousands of glyph masks -- and the workloads that need them most
21/// are the ones that miss steadily: text whose size animates re-rasterises
22/// every glyph of every frame, and every one of those inserts was walking the
23/// whole table to decide what to drop.
24///
25/// A key is held twice, once in the index and once in its slot, so an eviction
26/// can find the index entry to remove without searching for it. The keys these
27/// caches use are small `Copy` structs, and the duplicate is what keeps the
28/// links free of raw pointers.
29///
30/// The index and slots grow with the entries rather than reserving the bound:
31/// most caches of a process never come near it, and a table sized for
32/// thousands of entries each is megabytes a small screen never touches.
33pub struct BoundedLruCache<K, V> {
34    index: HashMap<K, usize>,
35    slots: Vec<Option<CacheSlot<K, V>>>,
36    free: Vec<usize>,
37    newest: Option<usize>,
38    oldest: Option<usize>,
39    cap: NonZeroUsize,
40}
41
42impl<K, V> BoundedLruCache<K, V>
43where
44    K: Clone + Eq + Hash,
45{
46    pub fn new(cap: NonZeroUsize) -> Self {
47        Self {
48            index: HashMap::default(),
49            slots: Vec::new(),
50            free: Vec::new(),
51            newest: None,
52            oldest: None,
53            cap,
54        }
55    }
56
57    pub fn with_capacity_at_least_one(cap: usize) -> Self {
58        let cap = NonZeroUsize::new(cap).unwrap_or(NonZeroUsize::MIN);
59        Self::new(cap)
60    }
61
62    pub fn len(&self) -> usize {
63        self.index.len()
64    }
65
66    pub fn is_empty(&self) -> bool {
67        self.index.is_empty()
68    }
69
70    pub fn cap(&self) -> NonZeroUsize {
71        self.cap
72    }
73
74    /// The lookups take any form of the key the stored key borrows as, so a
75    /// caller can probe with a borrowed view instead of building an owned key.
76    pub fn contains<Q>(&self, key: &Q) -> bool
77    where
78        K: Borrow<Q>,
79        Q: Hash + Eq + ?Sized,
80    {
81        self.index.contains_key(key)
82    }
83
84    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
85    where
86        K: Borrow<Q>,
87        Q: Hash + Eq + ?Sized,
88    {
89        let slot = *self.index.get(key)?;
90        self.promote(slot);
91        Some(&self.slot(slot).value)
92    }
93
94    pub fn peek<Q>(&self, key: &Q) -> Option<&V>
95    where
96        K: Borrow<Q>,
97        Q: Hash + Eq + ?Sized,
98    {
99        let slot = *self.index.get(key)?;
100        Some(&self.slot(slot).value)
101    }
102
103    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
104    where
105        K: Borrow<Q>,
106        Q: Hash + Eq + ?Sized,
107    {
108        let slot = *self.index.get(key)?;
109        self.promote(slot);
110        Some(&mut self.slot_mut(slot).value)
111    }
112
113    pub fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
114        if let Some(&slot) = self.index.get(&key) {
115            self.promote(slot);
116            let old_value = std::mem::replace(&mut self.slot_mut(slot).value, value);
117            return Some((key, old_value));
118        }
119
120        let evicted = if self.index.len() == self.cap.get() {
121            self.pop_lru()
122        } else {
123            None
124        };
125
126        let slot = self.claim_slot(key.clone(), value);
127        self.index.insert(key, slot);
128        self.link_newest(slot);
129        evicted
130    }
131
132    pub fn put(&mut self, key: K, value: V) -> Option<V> {
133        self.push(key, value).map(|(_, value)| value)
134    }
135
136    /// The least recently used entry, without touching its recency.
137    pub fn peek_lru(&self) -> Option<(&K, &V)> {
138        let entry = self.slot(self.oldest?);
139        Some((&entry.key, &entry.value))
140    }
141
142    pub fn pop_lru(&mut self) -> Option<(K, V)> {
143        let slot = self.oldest?;
144        self.unlink(slot);
145        let entry = self.release_slot(slot);
146        self.index.remove(&entry.key);
147        Some((entry.key, entry.value))
148    }
149
150    pub fn pop(&mut self, key: &K) -> Option<V> {
151        let slot = self.index.remove(key)?;
152        self.unlink(slot);
153        Some(self.release_slot(slot).value)
154    }
155
156    /// Entries most recently used first.
157    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
158        let mut next = self.newest;
159        std::iter::from_fn(move || {
160            let entry = self.slot(next?);
161            next = entry.older;
162            Some((&entry.key, &entry.value))
163        })
164    }
165
166    fn slot(&self, slot: usize) -> &CacheSlot<K, V> {
167        self.slots[slot]
168            .as_ref()
169            .expect("a linked cache slot is always occupied")
170    }
171
172    fn slot_mut(&mut self, slot: usize) -> &mut CacheSlot<K, V> {
173        self.slots[slot]
174            .as_mut()
175            .expect("a linked cache slot is always occupied")
176    }
177
178    fn promote(&mut self, slot: usize) {
179        if self.newest == Some(slot) {
180            return;
181        }
182        self.unlink(slot);
183        self.link_newest(slot);
184    }
185
186    fn link_newest(&mut self, slot: usize) {
187        let previous_newest = self.newest;
188        {
189            let entry = self.slot_mut(slot);
190            entry.newer = None;
191            entry.older = previous_newest;
192        }
193        if let Some(previous) = previous_newest {
194            self.slot_mut(previous).newer = Some(slot);
195        }
196        self.newest = Some(slot);
197        if self.oldest.is_none() {
198            self.oldest = Some(slot);
199        }
200    }
201
202    fn unlink(&mut self, slot: usize) {
203        let (newer, older) = {
204            let entry = self.slot_mut(slot);
205            (entry.newer.take(), entry.older.take())
206        };
207        match newer {
208            Some(newer) => self.slot_mut(newer).older = older,
209            None => self.newest = older,
210        }
211        match older {
212            Some(older) => self.slot_mut(older).newer = newer,
213            None => self.oldest = newer,
214        }
215    }
216
217    fn claim_slot(&mut self, key: K, value: V) -> usize {
218        let entry = CacheSlot {
219            key,
220            value,
221            newer: None,
222            older: None,
223        };
224        match self.free.pop() {
225            Some(slot) => {
226                self.slots[slot] = Some(entry);
227                slot
228            }
229            None => {
230                self.slots.push(Some(entry));
231                self.slots.len() - 1
232            }
233        }
234    }
235
236    fn release_slot(&mut self, slot: usize) -> CacheSlot<K, V> {
237        let entry = self.slots[slot]
238            .take()
239            .expect("a slot being released is always occupied");
240        self.free.push(slot);
241        entry
242    }
243}
244
245#[cfg(test)]
246#[path = "tests/bounded_lru_cache_tests.rs"]
247mod tests;