use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use vyre_foundation::ir::Program;
const DEFAULT_GRAPH_PLAN_CACHE_CAPACITY: usize = 128;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct GraphPlanCacheSnapshot {
pub entries: usize,
pub hits: u64,
pub misses: u64,
pub evictions: u64,
}
#[derive(Debug)]
pub(crate) struct GraphPlanCache<K> {
entries: HashMap<K, Program>,
lru: VecDeque<K>,
capacity: usize,
hits: u64,
misses: u64,
evictions: u64,
}
impl<K> Default for GraphPlanCache<K> {
fn default() -> Self {
Self {
entries: HashMap::new(),
lru: VecDeque::new(),
capacity: DEFAULT_GRAPH_PLAN_CACHE_CAPACITY,
hits: 0,
misses: 0,
evictions: 0,
}
}
}
impl<K> GraphPlanCache<K>
where
K: Copy + Eq + Hash,
{
#[cfg(test)]
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
..Self::default()
}
}
pub(crate) fn get_or_build(&mut self, key: K, build: impl FnOnce() -> Program) -> Program {
if let Some(program) = self.entries.get(&key).cloned() {
self.hits = self.hits.saturating_add(1);
self.touch(key);
return program;
}
self.misses = self.misses.saturating_add(1);
let program = build();
while self.entries.len() >= self.capacity {
let Some(victim) = self.lru.pop_front() else {
break;
};
if self.entries.remove(&victim).is_some() {
self.evictions = self.evictions.saturating_add(1);
break;
}
}
self.entries.insert(key, program.clone());
self.lru.push_back(key);
program
}
#[must_use]
pub(crate) fn snapshot(&self) -> GraphPlanCacheSnapshot {
GraphPlanCacheSnapshot {
entries: self.entries.len(),
hits: self.hits,
misses: self.misses,
evictions: self.evictions,
}
}
fn touch(&mut self, key: K) {
if let Some(index) = self.lru.iter().position(|candidate| *candidate == key) {
self.lru.remove(index);
}
self.lru.push_back(key);
}
}
#[cfg(test)]
mod tests {
use super::*;
use vyre_foundation::ir::{Node, Program};
fn empty_program() -> Program {
Program::wrapped(Vec::new(), [1, 1, 1], vec![Node::Return])
}
#[test]
fn cache_hits_reuse_existing_program_and_update_counters() {
let mut cache = GraphPlanCache::with_capacity(4);
cache.get_or_build(7u32, empty_program);
cache.get_or_build(7u32, || panic!("cache hit must not rebuild"));
let snapshot = cache.snapshot();
assert_eq!(snapshot.entries, 1);
assert_eq!(snapshot.hits, 1);
assert_eq!(snapshot.misses, 1);
assert_eq!(snapshot.evictions, 0);
}
#[test]
fn cache_eviction_is_bounded_and_lru() {
let mut cache = GraphPlanCache::with_capacity(2);
cache.get_or_build(1u32, empty_program);
cache.get_or_build(2u32, empty_program);
cache.get_or_build(1u32, empty_program);
cache.get_or_build(3u32, empty_program);
let snapshot = cache.snapshot();
assert_eq!(snapshot.entries, 2);
assert_eq!(snapshot.hits, 1);
assert_eq!(snapshot.misses, 3);
assert_eq!(snapshot.evictions, 1);
cache.get_or_build(2u32, empty_program);
let snapshot = cache.snapshot();
assert_eq!(snapshot.misses, 4, "key 2 should have been evicted first");
}
}