vyre_driver_wgpu/runtime/cache/
lru.rs1use rustc_hash::FxHashMap;
2
3use crate::allocation::{reserve_hash_map_to_capacity, reserve_vec_to_capacity};
4
5pub const DEFAULT_INTRUSIVE_LRU_CAPACITY: usize = 65_536;
7
8pub 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 #[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 #[inline]
55 pub fn try_new() -> Result<Self, vyre_driver::BackendError> {
56 Self::try_with_reserved_capacity(DEFAULT_INTRUSIVE_LRU_CAPACITY)
57 }
58
59 #[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 #[inline]
86 pub fn try_with_capacity(capacity: usize) -> Result<Self, vyre_driver::BackendError> {
87 let capacity = capacity.max(1);
90 Self::try_with_capacity_policy(capacity, Some(capacity))
91 }
92
93 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Copy, Default)]
352pub struct AccessMeta {
353 pub frequency: u32,
355 pub size: u64,
357 pub last_access: u64,
359}
360
361#[non_exhaustive]
363pub struct AccessTracker {
364 lru: IntrusiveLru<u64, AccessMeta>,
365 tick: u64,
366}
367
368impl AccessTracker {
369 #[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 #[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 #[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 #[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 #[inline]
433 pub fn stats(&self, key: u64) -> Option<crate::runtime::cache::AccessStats> {
434 let meta = self.get_meta(key)?;
435 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}