Skip to main content

vyre_driver_wgpu/runtime/cache/
lru.rs

1use rustc_hash::FxHashMap;
2
3use crate::allocation::{reserve_hash_map_to_capacity, reserve_vec_to_capacity};
4
5/// Default initial node reservation used by [`IntrusiveLru`].
6pub const DEFAULT_INTRUSIVE_LRU_CAPACITY: usize = 65_536;
7
8/// Intrusive doubly-linked LRU over a slab allocator.
9///
10/// O(1) record, remove, and hottest/coldest iteration.
11pub struct IntrusiveLru<K, V> {
12    nodes: Vec<Node<K, V>>,
13    indices: FxHashMap<K, usize>,
14    free: Vec<usize>,
15    head: Option<usize>,
16    tail: Option<usize>,
17    live_limit: Option<usize>,
18}
19
20struct Node<K, V> {
21    key: K,
22    value: V,
23    prev: Option<usize>,
24    next: Option<usize>,
25    active: bool,
26}
27
28impl<K, V> IntrusiveLru<K, V>
29where
30    K: std::hash::Hash + Eq + Copy,
31    V: Default,
32{
33    /// Create an LRU with the default live-node capacity.
34    #[inline]
35    pub fn new() -> Self {
36        match Self::try_new() {
37            Ok(lru) => lru,
38            Err(error) => {
39                tracing::error!(
40                    error = %error,
41                    "wgpu intrusive LRU default reservation failed; continuing with grow-on-use storage"
42                );
43                Self::empty_with_policy(None)
44            }
45        }
46    }
47
48    /// Fallible version of [`Self::new`].
49    ///
50    /// # Errors
51    ///
52    /// Returns [`vyre_driver::BackendError`] if default LRU backing storage
53    /// cannot be reserved.
54    #[inline]
55    pub fn try_new() -> Result<Self, vyre_driver::BackendError> {
56        Self::try_with_reserved_capacity(DEFAULT_INTRUSIVE_LRU_CAPACITY)
57    }
58
59    /// Create an LRU with a fixed live-node capacity.
60    ///
61    /// A zero capacity is clamped to one so externally-derived
62    /// capacity budgets cannot disable the LRU by accident.
63    #[inline]
64    pub fn with_capacity(capacity: usize) -> Self {
65        let capacity = capacity.max(1);
66        match Self::try_with_capacity(capacity) {
67            Ok(lru) => lru,
68            Err(error) => {
69                tracing::error!(
70                    capacity,
71                    error = %error,
72                    "wgpu intrusive LRU bounded reservation failed; continuing with grow-on-use storage"
73                );
74                Self::empty_with_policy(Some(capacity))
75            }
76        }
77    }
78
79    /// Fallible version of [`Self::with_capacity`].
80    ///
81    /// # Errors
82    ///
83    /// Returns [`vyre_driver::BackendError`] if LRU backing storage cannot be
84    /// reserved.
85    #[inline]
86    pub fn try_with_capacity(capacity: usize) -> Result<Self, vyre_driver::BackendError> {
87        // Defensive: a capacity of 0 would make the LRU unusable; clamp to 1
88        // so callers that compute capacity from external config never panic.
89        let capacity = capacity.max(1);
90        Self::try_with_capacity_policy(capacity, Some(capacity))
91    }
92
93    /// Create an LRU that reserves `capacity` slots but does not silently evict
94    /// live nodes when the reservation is exceeded.
95    ///
96    /// Cache metadata uses this path because the owning cache, not the LRU
97    /// backing store, defines when an entry is evicted. Dropping metadata while
98    /// the cache entry is still live would make promotion stats disappear and
99    /// force cold-path scans at scale.
100    #[inline]
101    pub fn with_reserved_capacity(capacity: usize) -> Self {
102        match Self::try_with_reserved_capacity(capacity) {
103            Ok(lru) => lru,
104            Err(error) => {
105                tracing::error!(
106                    capacity,
107                    error = %error,
108                    "wgpu intrusive LRU reservation failed; continuing with grow-on-use storage"
109                );
110                Self::empty_with_policy(None)
111            }
112        }
113    }
114
115    /// Fallible version of [`Self::with_reserved_capacity`].
116    ///
117    /// # Errors
118    ///
119    /// Returns [`vyre_driver::BackendError`] if LRU backing storage cannot be
120    /// reserved.
121    #[inline]
122    pub fn try_with_reserved_capacity(capacity: usize) -> Result<Self, vyre_driver::BackendError> {
123        let capacity = capacity.max(1);
124        Self::try_with_capacity_policy(capacity, None)
125    }
126
127    fn try_with_capacity_policy(
128        capacity: usize,
129        live_limit: Option<usize>,
130    ) -> Result<Self, vyre_driver::BackendError> {
131        let mut nodes = Vec::new();
132        reserve_vec_to_capacity(
133            &mut nodes,
134            capacity,
135            "wgpu intrusive LRU",
136            "node slot",
137            "reduce runtime cache capacity or shard cache metadata",
138        )?;
139        let mut indices = FxHashMap::default();
140        reserve_hash_map_to_capacity(
141            &mut indices,
142            capacity,
143            "wgpu intrusive LRU",
144            "index entry",
145            "reduce runtime cache capacity or shard cache metadata",
146        )?;
147        let mut free = Vec::new();
148        reserve_vec_to_capacity(
149            &mut free,
150            capacity,
151            "wgpu intrusive LRU",
152            "free-list slot",
153            "reduce runtime cache capacity or shard cache metadata",
154        )?;
155        Ok(Self {
156            nodes,
157            indices,
158            free,
159            head: None,
160            tail: None,
161            live_limit,
162        })
163    }
164
165    fn empty_with_policy(live_limit: Option<usize>) -> Self {
166        Self {
167            nodes: Vec::new(),
168            indices: FxHashMap::default(),
169            free: Vec::new(),
170            head: None,
171            tail: None,
172            live_limit,
173        }
174    }
175
176    /// Ensure a node exists for `key` and return a mutable value reference.
177    #[inline]
178    pub fn ensure(&mut self, key: K) -> &mut V {
179        if let Some(&index) = self.indices.get(&key) {
180            return &mut self.nodes[index].value;
181        }
182        let index = self.alloc_node(key);
183        &mut self.nodes[index].value
184    }
185
186    /// Ensure a node exists for `key`, move it to the hot end, and
187    /// return a mutable value reference.
188    #[inline]
189    pub fn ensure_front(&mut self, key: K) -> &mut V {
190        let index = if let Some(&index) = self.indices.get(&key) {
191            self.move_to_front(index);
192            index
193        } else {
194            self.alloc_node(key)
195        };
196        &mut self.nodes[index].value
197    }
198
199    /// Move `key` to the front if it is present.
200    #[inline]
201    pub fn touch(&mut self, key: K) {
202        if let Some(&index) = self.indices.get(&key) {
203            self.move_to_front(index);
204        }
205    }
206
207    /// Remove a key if it is present.
208    #[inline]
209    pub fn remove(&mut self, key: &K) {
210        let Some(index) = self.indices.remove(key) else {
211            return;
212        };
213        self.detach(index);
214        let node = &mut self.nodes[index];
215        node.active = false;
216        self.free.push(index);
217    }
218
219    /// Return the value for `key` if it is currently active.
220    #[inline]
221    pub fn get(&self, key: &K) -> Option<&V> {
222        let &index = self.indices.get(key)?;
223        let node = &self.nodes[index];
224        node.active.then_some(&node.value)
225    }
226
227    /// Return the `n` hottest keys in most-recent-first order.
228    #[inline]
229    pub fn hottest(&self, n: usize) -> Vec<K> {
230        let mut keys = Vec::new();
231        keys.extend(self.iter_hottest().map(|(key, _)| *key).take(n));
232        keys
233    }
234
235    /// Iterate entries from most recent to least recent.
236    #[inline]
237    pub fn iter_hottest(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
238        let mut current = self.head;
239        std::iter::from_fn(move || {
240            let index = current?;
241            let node = &self.nodes[index];
242            current = node.next;
243            Some((&node.key, &node.value))
244        })
245    }
246
247    /// Iterate entries from least recent to most recent.
248    #[inline]
249    pub fn iter_coldest(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
250        let mut current = self.tail;
251        std::iter::from_fn(move || {
252            let index = current?;
253            let node = &self.nodes[index];
254            current = node.prev;
255            Some((&node.key, &node.value))
256        })
257    }
258
259    fn alloc_node(&mut self, key: K) -> usize {
260        if self.live_limit == Some(self.indices.len()) {
261            if let Some(coldest) = self.tail {
262                let evicted_key = self.nodes[coldest].key;
263                self.remove(&evicted_key);
264            }
265        }
266        let index = if let Some(index) = self.free.pop() {
267            self.nodes[index] = Node {
268                key,
269                value: V::default(),
270                prev: None,
271                next: None,
272                active: true,
273            };
274            index
275        } else {
276            self.nodes.push(Node {
277                key,
278                value: V::default(),
279                prev: None,
280                next: None,
281                active: true,
282            });
283            self.nodes.len() - 1
284        };
285        self.indices.insert(key, index);
286        self.attach_front(index);
287        index
288    }
289
290    /// Return backing-store capacities for cache diagnostics.
291    ///
292    /// This is intentionally public rather than test-only so structure
293    /// contracts do not need inline test-only hooks in production modules.
294    #[doc(hidden)]
295    pub fn reserved_capacity_for_diagnostics(&self) -> (usize, usize, usize) {
296        (
297            self.nodes.capacity(),
298            self.indices.capacity(),
299            self.free.capacity(),
300        )
301    }
302
303    fn move_to_front(&mut self, index: usize) {
304        if self.head == Some(index) {
305            return;
306        }
307        self.detach(index);
308        self.attach_front(index);
309    }
310
311    fn attach_front(&mut self, index: usize) {
312        self.nodes[index].prev = None;
313        self.nodes[index].next = self.head;
314        if let Some(head) = self.head {
315            self.nodes[head].prev = Some(index);
316        } else {
317            self.tail = Some(index);
318        }
319        self.head = Some(index);
320    }
321
322    fn detach(&mut self, index: usize) {
323        let prev = self.nodes[index].prev;
324        let next = self.nodes[index].next;
325        if let Some(prev) = prev {
326            self.nodes[prev].next = next;
327        } else if self.head == Some(index) {
328            self.head = next;
329        }
330        if let Some(next) = next {
331            self.nodes[next].prev = prev;
332        } else if self.tail == Some(index) {
333            self.tail = prev;
334        }
335        self.nodes[index].prev = None;
336        self.nodes[index].next = None;
337    }
338}
339
340impl<K, V> Default for IntrusiveLru<K, V>
341where
342    K: std::hash::Hash + Eq + Copy,
343    V: Default,
344{
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350/// Metadata attached to each LRU node inside [`AccessTracker`].
351#[derive(Debug, Clone, Copy, Default)]
352pub struct AccessMeta {
353    /// Number of recorded accesses.
354    pub frequency: u32,
355    /// Entry size in bytes.
356    pub size: u64,
357    /// Monotonic tick recorded for the last access.
358    pub last_access: u64,
359}
360
361/// Tracks access patterns for cache entries.
362#[non_exhaustive]
363pub struct AccessTracker {
364    lru: IntrusiveLru<u64, AccessMeta>,
365    tick: u64,
366}
367
368impl AccessTracker {
369    /// Create a new empty tracker.
370    #[inline]
371    pub fn new() -> Self {
372        match Self::try_new() {
373            Ok(tracker) => tracker,
374            Err(error) => {
375                tracing::error!(
376                    error = %error,
377                    "wgpu access tracker reservation failed; continuing with grow-on-use storage"
378                );
379                Self {
380                    lru: IntrusiveLru::empty_with_policy(None),
381                    tick: 0,
382                }
383            }
384        }
385    }
386
387    /// Fallible version of [`Self::new`].
388    ///
389    /// # Errors
390    ///
391    /// Returns [`vyre_driver::BackendError`] if tracker backing storage cannot
392    /// be reserved.
393    #[inline]
394    pub fn try_new() -> Result<Self, vyre_driver::BackendError> {
395        Ok(Self {
396            lru: IntrusiveLru::try_new()?,
397            tick: 0,
398        })
399    }
400
401    /// Record an access for the given key.
402    #[inline]
403    pub fn record(&mut self, key: u64) {
404        self.advance_tick();
405        let meta = self.lru.ensure_front(key);
406        meta.frequency = bounded_frequency_increment(meta.frequency);
407        meta.last_access = self.tick;
408    }
409
410    /// Return the `n` hottest keys in most-recent-first order.
411    #[inline]
412    pub fn hot_set(&self, n: usize) -> Vec<u64> {
413        self.lru.hottest(n)
414    }
415
416    #[inline]
417    pub(crate) fn set_size(&mut self, key: u64, size: u64) {
418        self.lru.ensure(key).size = size;
419    }
420
421    #[inline]
422    pub(crate) fn remove(&mut self, key: u64) {
423        self.lru.remove(&key);
424    }
425
426    #[inline]
427    pub(crate) fn get_meta(&self, key: u64) -> Option<&AccessMeta> {
428        self.lru.get(&key)
429    }
430
431    /// Return access statistics for a key.
432    #[inline]
433    pub fn stats(&self, key: u64) -> Option<crate::runtime::cache::AccessStats> {
434        let meta = self.get_meta(key)?;
435        // O(1) relative-recency via monotonic tick counter instead of O(N)
436        // linear scan through the intrusive list.
437        Some(crate::runtime::cache::AccessStats {
438            frequency: meta.frequency,
439            last_access: meta.last_access,
440            size: meta.size,
441        })
442    }
443
444    fn advance_tick(&mut self) {
445        if let Some(next) = self.tick.checked_add(1) {
446            self.tick = next;
447            return;
448        }
449        self.rebase_ticks_by_lru_order();
450        self.tick = match self.tick.checked_add(1) {
451            Some(next) => next,
452            None => u64::MAX,
453        };
454    }
455
456    fn rebase_ticks_by_lru_order(&mut self) {
457        let mut current = self.lru.tail;
458        let mut tick = 0_u64;
459        while let Some(index) = current {
460            let next = self.lru.nodes[index].prev;
461            if self.lru.nodes[index].active {
462                tick = match tick.checked_add(1) {
463                    Some(next_tick) => next_tick,
464                    None => u64::MAX,
465                };
466                self.lru.nodes[index].value.last_access = tick;
467            }
468            current = next;
469        }
470        self.tick = tick;
471    }
472}
473
474fn bounded_frequency_increment(value: u32) -> u32 {
475    match value.checked_add(1) {
476        Some(next) => next,
477        None => u32::MAX,
478    }
479}
480
481impl Default for AccessTracker {
482    fn default() -> Self {
483        Self::new()
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn intrusive_lru_constructors_use_shared_fallible_reservation() {
493        let bounded = IntrusiveLru::<u64, AccessMeta>::try_with_capacity(4)
494            .expect("Fix: bounded LRU capacity should reserve");
495        let reserved = IntrusiveLru::<u64, AccessMeta>::try_with_reserved_capacity(4)
496            .expect("Fix: reserved LRU capacity should reserve");
497
498        assert!(bounded.reserved_capacity_for_diagnostics().0 >= 4);
499        assert!(reserved.reserved_capacity_for_diagnostics().0 >= 4);
500
501        let production = include_str!("lru.rs")
502            .split("#[cfg(test)]")
503            .next()
504            .expect("Fix: lru.rs must contain production section");
505        assert!(
506            production.contains("fn try_with_capacity_policy")
507                && production.contains("reserve_vec_to_capacity")
508                && production.contains("reserve_hash_map_to_capacity")
509                && production.contains("pub fn try_new()")
510                && !production.contains("Vec::with_capacity")
511                && !production.contains("FxHashMap::with_capacity_and_hasher"),
512            "Fix: WGPU runtime LRU constructors must share fallible reservation rather than duplicating infallible capacity constructors."
513        );
514        assert!(
515            !production.contains(".expect("),
516            "Fix: WGPU runtime LRU production constructors must not panic on allocation pressure."
517        );
518    }
519
520    #[test]
521    fn access_tracker_rebases_ticks_in_lru_order_instead_of_panicking() {
522        let mut tracker = AccessTracker::new();
523        tracker.record(10);
524        tracker.record(20);
525        tracker.record(30);
526        tracker.tick = u64::MAX;
527
528        tracker.record(20);
529
530        assert_eq!(tracker.hot_set(3), vec![20, 30, 10]);
531        let hot = tracker.stats(20).expect("Fix: hot key must remain tracked");
532        let warm = tracker
533            .stats(30)
534            .expect("Fix: warm key must remain tracked");
535        let cold = tracker
536            .stats(10)
537            .expect("Fix: cold key must remain tracked");
538        assert!(hot.last_access > warm.last_access);
539        assert!(warm.last_access > cold.last_access);
540    }
541
542    #[test]
543    fn access_tracker_frequency_pins_instead_of_panicking() {
544        let mut tracker = AccessTracker::new();
545        tracker.record(7);
546        tracker.lru.ensure(7).frequency = u32::MAX;
547
548        tracker.record(7);
549
550        assert_eq!(
551            tracker
552                .stats(7)
553                .expect("Fix: tracked key must have stats")
554                .frequency,
555            u32::MAX
556        );
557    }
558
559    #[test]
560    fn access_tracker_source_has_no_release_path_panic_counters() {
561        let source = include_str!("lru.rs");
562        let production = source
563            .split("#[cfg(test)]")
564            .next()
565            .expect("Fix: LRU production source must precede tests");
566        assert!(
567            !production.contains(concat!("panic", "!("))
568                && !production.contains(".unwrap_or_else("),
569            "Fix: runtime cache LRU counters must rebase or pin instead of aborting."
570        );
571        assert!(
572            production.contains("rebase_ticks_by_lru_order")
573                && production.contains("bounded_frequency_increment"),
574            "Fix: runtime cache LRU must preserve recency across tick exhaustion and pin access frequency."
575        );
576    }
577}