use alloc::vec::Vec;
use hashbrown::{HashMap, HashSet};
use crate::id::GraphId;
pub type InfoCacheKey = Vec<u64>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheMode {
Normal,
Capture,
}
#[derive(Debug, Clone, Copy)]
pub struct MetadataCachePolicy {
max_entries: usize,
max_cached_size: usize,
mode: CacheMode,
}
impl Default for MetadataCachePolicy {
fn default() -> Self {
Self::new(4096, 2048)
}
}
impl MetadataCachePolicy {
pub fn new(max_entries: usize, max_cached_size: usize) -> Self {
Self {
max_entries,
max_cached_size,
mode: CacheMode::Normal,
}
}
pub fn mode(&mut self, mode: CacheMode) {
self.mode = mode;
}
pub fn should_cache(&self, size: usize) -> bool {
match self.mode {
CacheMode::Capture => true,
CacheMode::Normal => self.max_entries > 0 && size <= self.max_cached_size,
}
}
pub fn capacity(&self) -> Option<usize> {
match self.mode {
CacheMode::Capture => None,
CacheMode::Normal => Some(self.max_entries),
}
}
pub fn pins_entries(&self) -> bool {
matches!(self.mode, CacheMode::Capture)
}
}
#[derive(Debug)]
struct Entry<V> {
value: V,
last_used: u64,
locks: u32,
}
#[derive(Debug)]
pub struct MetadataInfoCache<V> {
entries: HashMap<InfoCacheKey, Entry<V>>,
policy: MetadataCachePolicy,
clock: u64,
pending: HashSet<InfoCacheKey>,
graph_locks: HashMap<GraphId, Vec<InfoCacheKey>>,
}
impl<V> MetadataInfoCache<V> {
pub fn new(policy: MetadataCachePolicy) -> Self {
Self {
entries: HashMap::new(),
policy,
clock: 0,
pending: HashSet::new(),
graph_locks: HashMap::new(),
}
}
pub fn mode(&mut self, mode: CacheMode) {
self.policy.mode(mode);
}
pub fn should_cache(&self, size: usize) -> bool {
self.policy.should_cache(size)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl<V: Clone> MetadataInfoCache<V> {
pub fn get(&mut self, key: &[u64]) -> Option<V> {
self.clock += 1;
let clock = self.clock;
let pin = self.policy.pins_entries() && !self.pending.contains(key);
let entry = self.entries.get_mut(key)?;
entry.last_used = clock;
if pin {
self.pending.insert(key.to_vec());
entry.locks += 1;
}
Some(entry.value.clone())
}
pub fn insert(&mut self, key: InfoCacheKey, value: V) {
if let Some(capacity) = self.policy.capacity() {
if capacity == 0 {
return;
}
if self.entries.len() >= capacity {
self.evict_least_recently_used();
}
}
let locks = if self.policy.pins_entries() {
self.pending.insert(key.clone());
1
} else {
0
};
self.entries.insert(
key,
Entry {
value,
last_used: self.clock,
locks,
},
);
}
pub fn capture_commit(&mut self, graph: GraphId) {
if !self.pending.is_empty() {
let keys = self.pending.drain().collect();
self.graph_locks.insert(graph, keys);
}
}
pub fn capture_discard(&mut self) {
let keys: Vec<_> = self.pending.drain().collect();
for key in keys {
if let Some(entry) = self.entries.get_mut(&key) {
entry.locks = entry.locks.saturating_sub(1);
}
}
}
pub fn graph_release(&mut self, graph: GraphId) {
let Some(keys) = self.graph_locks.remove(&graph) else {
return;
};
for key in keys {
let drop_entry = match self.entries.get_mut(&key) {
Some(entry) => {
entry.locks = entry.locks.saturating_sub(1);
entry.locks == 0
}
None => false,
};
if drop_entry {
self.entries.remove(&key);
}
}
}
fn evict_least_recently_used(&mut self) {
let victim = self
.entries
.iter()
.filter(|(_, entry)| entry.locks == 0)
.min_by_key(|(_, entry)| entry.last_used)
.map(|(key, _)| key.clone());
if let Some(key) = victim {
self.entries.remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(n: u64) -> InfoCacheKey {
alloc::vec![n]
}
fn cache(max_entries: usize) -> MetadataInfoCache<u32> {
MetadataInfoCache::new(MetadataCachePolicy::new(max_entries, 64))
}
#[test]
fn normal_mode_gates_on_size() {
let cache = cache(8);
assert!(cache.should_cache(64), "within max_cached_size");
assert!(!cache.should_cache(65), "over max_cached_size");
}
#[test]
fn capture_mode_caches_any_size() {
let mut cache = cache(8);
cache.mode(CacheMode::Capture);
assert!(cache.should_cache(10_000));
}
#[test]
fn hit_returns_value_and_records_use() {
let mut cache = cache(8);
let k = key(1);
assert!(cache.get(&k).is_none());
cache.insert(k.clone(), 42);
assert_eq!(cache.get(&k), Some(42));
}
#[test]
fn normal_mode_evicts_least_recently_used() {
let mut cache = cache(2);
cache.insert(key(0), 0);
cache.insert(key(1), 1);
assert_eq!(cache.get(&key(0)), Some(0));
cache.insert(key(2), 2);
assert_eq!(cache.len(), 2, "stayed at capacity");
assert_eq!(cache.get(&key(0)), Some(0), "recently used entry kept");
assert!(cache.get(&key(1)).is_none(), "least-recently-used evicted");
assert_eq!(cache.get(&key(2)), Some(2), "new entry cached");
}
#[test]
fn capture_mode_is_unbounded() {
let mut cache = cache(2);
cache.mode(CacheMode::Capture);
for i in 0..10 {
cache.insert(key(i), i as u32);
}
assert_eq!(cache.len(), 10, "capture must cache every buffer");
}
#[test]
fn zero_capacity_disables_caching() {
let mut cache = cache(0);
assert!(!cache.should_cache(8), "disabled cache never caches");
cache.insert(key(0), 0);
assert!(cache.is_empty());
}
#[test]
fn pinned_entry_survives_eviction_until_graph_released() {
let mut cache = cache(2);
let graph = GraphId::new();
cache.mode(CacheMode::Capture);
cache.insert(key(0), 0);
cache.capture_commit(graph);
cache.mode(CacheMode::Normal);
for i in 1..20 {
cache.get(&key(i));
cache.insert(key(i), i as u32);
}
assert_eq!(cache.get(&key(0)), Some(0), "pinned entry never evicted");
cache.graph_release(graph);
assert!(cache.get(&key(0)).is_none(), "released entry cleaned up");
}
#[test]
fn capture_hit_on_normal_entry_pins_it() {
let mut cache = cache(2);
let graph = GraphId::new();
cache.insert(key(0), 0);
cache.mode(CacheMode::Capture);
assert_eq!(cache.get(&key(0)), Some(0));
cache.capture_commit(graph);
cache.mode(CacheMode::Normal);
for i in 1..20 {
cache.get(&key(i));
cache.insert(key(i), i as u32);
}
assert_eq!(cache.get(&key(0)), Some(0), "hit-pinned entry survives");
}
#[test]
fn pin_is_refcounted_across_graphs() {
let mut cache = cache(8);
let (g1, g2) = (GraphId::new(), GraphId::new());
cache.mode(CacheMode::Capture);
cache.insert(key(0), 0);
cache.capture_commit(g1);
assert_eq!(cache.get(&key(0)), Some(0));
cache.capture_commit(g2);
cache.mode(CacheMode::Normal);
cache.graph_release(g1);
assert_eq!(cache.get(&key(0)), Some(0), "still pinned by g2");
cache.graph_release(g2);
assert!(cache.get(&key(0)).is_none(), "gone once both released");
}
#[test]
fn capture_discard_unpins_without_removing() {
let mut cache = cache(8);
cache.mode(CacheMode::Capture);
cache.insert(key(0), 0);
cache.capture_discard();
cache.mode(CacheMode::Normal);
assert_eq!(cache.get(&key(0)), Some(0), "kept as a normal entry");
for i in 1..20 {
cache.get(&key(i));
cache.insert(key(i), i as u32);
}
assert!(cache.get(&key(0)).is_none(), "no longer pinned, evicted");
}
}