Skip to main content

weir/fixed_point_resident_cache/
cache.rs

1use super::{
2    entry::FixedPointResidentGraphCacheEntry, retained_graph_bytes,
3    stats::FixedPointResidentGraphCacheStats,
4};
5use crate::fixed_point_graph::FixedPointForwardGraph;
6use crate::fixed_point_resident::FixedPointResidentGraph;
7use crate::resident_cache_identity::{
8    ResidentGraphCacheIdentity, ResidentGraphCacheMissEvidence, ResidentGraphCacheMissReason,
9};
10use crate::resident_cache_lru::ResidentCacheLru;
11use std::collections::HashMap;
12use vyre::ResidentGraphReuseTelemetry;
13use vyre_primitives::bitset::bitset_words;
14
15/// Backend-resident graph cache keyed by backend identity and normalized graph
16/// layout.
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct FixedPointResidentGraphCache {
19    entries: HashMap<ResidentGraphCacheIdentity, FixedPointResidentGraphCacheEntry>,
20    lru: ResidentCacheLru<ResidentGraphCacheIdentity>,
21    max_retained_bytes: Option<usize>,
22    retained_bytes: usize,
23    clock: u64,
24    hits: u64,
25    misses: u64,
26    resident_uploads: u64,
27    resident_graph_reuse: ResidentGraphReuseTelemetry,
28    evictions: u64,
29    last_miss_reason: Option<ResidentGraphCacheMissReason>,
30}
31
32impl FixedPointResidentGraphCache {
33    /// Create an empty resident graph cache.
34    #[must_use]
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Create a resident graph cache with a soft upper bound on retained bytes.
40    ///
41    /// The newest graph is always retained so one oversized graph may exceed
42    /// the limit; older least-recently-used entries are evicted first.
43    #[must_use]
44    pub fn with_max_retained_bytes(max_retained_bytes: usize) -> Self {
45        Self {
46            max_retained_bytes: Some(max_retained_bytes),
47            ..Self::default()
48        }
49    }
50
51    /// Return a resident graph for `graph`, uploading it only on a cache miss.
52    pub fn get_or_upload(
53        &mut self,
54        backend: &dyn vyre::VyreBackend,
55        graph: &FixedPointForwardGraph,
56    ) -> Result<&FixedPointResidentGraph, vyre::BackendError> {
57        let backend_id = backend.id();
58        let backend_version = backend.version();
59        let layout_hash = graph.stable_layout_hash();
60        let identity = ResidentGraphCacheIdentity::fixed_point(
61            backend_id,
62            backend_version,
63            graph.kind(),
64            layout_hash,
65            graph.node_count(),
66            graph.edge_count(),
67            bitset_words(graph.node_count()),
68        );
69        if let Some(entry) = self.entries.get(&identity) {
70            let retained_bytes = entry.retained_bytes;
71            if entry.graph.node_count() != graph.node_count()
72                || entry.graph.edge_count() != graph.edge_count()
73                || entry.graph.kind() != graph.kind()
74                || entry.graph.stable_layout_hash() != layout_hash
75            {
76                return Err(vyre::BackendError::InvalidProgram {
77                    fix: format!(
78                        "Fix: resident fixed-point graph cache entry for backend `{}` version `{}` has layout_hash={} nodes={} edges={} kind={:?} but requested layout_hash={} nodes={} edges={} kind={:?}; rebuild the resident graph cache before dispatch.",
79                        backend_id,
80                        backend_version,
81                        entry.graph.stable_layout_hash(),
82                        entry.graph.node_count(),
83                        entry.graph.edge_count(),
84                        entry.graph.kind(),
85                        layout_hash,
86                        graph.node_count(),
87                        graph.edge_count(),
88                        graph.kind()
89                    ),
90                });
91            }
92            self.last_miss_reason = None;
93            self.hits = self
94                .hits
95                .checked_add(1)
96                .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache hit counter overflowed u64; rebuild the resident graph cache before reuse."))?;
97            let retained_bytes_u64 = u64::try_from(retained_bytes).map_err(|error| {
98                resident_graph_cache_error(format!(
99                    "Fix: resident fixed-point graph retained byte count does not fit u64 during hit accounting: {error}; shard the graph before resident reuse."
100                ))
101            })?;
102            self.resident_graph_reuse
103                .record_warm_reuse(retained_bytes_u64)
104                .map_err(|error| resident_graph_cache_error(error.to_string()))?;
105            let last_seen = self.next_cache_tick()?;
106            self.record_lru(identity.clone(), last_seen)?;
107            let entry = self.entries.get_mut(&identity).ok_or_else(|| {
108                resident_graph_cache_error("Fix: resident graph cache key disappeared after LRU refresh; rebuild the resident graph cache because this indicates internal cache state corruption.")
109            })?;
110            entry.last_seen = last_seen;
111            return Ok(&entry.graph);
112        }
113        self.last_miss_reason = Some(self.miss_reason_for_identity(&identity));
114        self.misses = self
115            .misses
116            .checked_add(1)
117            .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache miss counter overflowed u64; rebuild the resident graph cache before reuse."))?;
118        let retained_bytes = retained_graph_bytes(graph)?;
119        self.evict_until_room(backend, retained_bytes)?;
120        let resident = FixedPointResidentGraph::upload(backend, graph)?;
121        self.resident_uploads = self
122            .resident_uploads
123            .checked_add(1)
124            .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache upload counter overflowed u64; rebuild the resident graph cache before reuse."))?;
125        let retained_bytes_u64 = u64::try_from(retained_bytes).map_err(|error| {
126            resident_graph_cache_error(format!(
127                "Fix: resident fixed-point graph retained byte count does not fit u64: {error}; shard the graph before resident upload."
128            ))
129        })?;
130        self.resident_graph_reuse
131            .record_cold_upload(retained_bytes_u64)
132            .map_err(|error| resident_graph_cache_error(error.to_string()))?;
133        let last_seen = self.next_cache_tick()?;
134        self.entries.insert(
135            identity.clone(),
136            FixedPointResidentGraphCacheEntry {
137                retained_bytes,
138                last_seen,
139                graph: resident,
140            },
141        );
142        self.record_lru(identity.clone(), last_seen)?;
143        self.retained_bytes = self
144            .retained_bytes
145            .checked_add(retained_bytes)
146            .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache retained byte accounting overflowed usize; reduce retained graph budget or rebuild the resident graph cache."))?;
147        Ok(&self
148            .entries
149            .get(&identity)
150            .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache key disappeared after upload; rebuild the resident graph cache because this indicates internal cache state corruption."))?
151            .graph)
152    }
153
154    /// Number of resident graph entries retained by the cache.
155    #[must_use]
156    pub fn len(&self) -> usize {
157        self.entries.len()
158    }
159
160    /// Whether the cache is empty.
161    #[must_use]
162    pub fn is_empty(&self) -> bool {
163        self.entries.is_empty()
164    }
165
166    /// Return point-in-time resident graph cache counters.
167    #[must_use]
168    pub fn stats(&self) -> FixedPointResidentGraphCacheStats {
169        FixedPointResidentGraphCacheStats {
170            hits: self.hits,
171            misses: self.misses,
172            resident_uploads: self.resident_uploads,
173            resident_upload_bytes: self.resident_graph_reuse.upload_bytes,
174            resident_avoided_upload_bytes: self.resident_graph_reuse.avoided_upload_bytes,
175            evictions: self.evictions,
176            retained_bytes: self.retained_bytes,
177            entries: self.entries.len(),
178        }
179    }
180
181    /// Deterministic cache identity snapshot for diagnostics and release
182    /// evidence.
183    #[must_use]
184    pub fn resident_identities(&self) -> Vec<ResidentGraphCacheIdentity> {
185        let mut identities = self.entries.keys().cloned().collect::<Vec<_>>();
186        identities.sort_by(ResidentGraphCacheIdentity::cmp_stable);
187        identities
188    }
189
190    /// Explain why `requested` would miss the current resident graph cache.
191    #[must_use]
192    pub fn miss_reason_for_identity(
193        &self,
194        requested: &ResidentGraphCacheIdentity,
195    ) -> ResidentGraphCacheMissReason {
196        ResidentGraphCacheMissReason::classify(ResidentGraphCacheMissEvidence::from_identities(
197            self.entries.keys(),
198            requested,
199        ))
200    }
201
202    /// Reason attached to the most recent cache miss. Hits clear this field.
203    #[must_use]
204    pub const fn last_miss_reason(&self) -> Option<ResidentGraphCacheMissReason> {
205        self.last_miss_reason
206    }
207
208    fn evict_until_room(
209        &mut self,
210        backend: &dyn vyre::VyreBackend,
211        incoming_bytes: usize,
212    ) -> Result<(), vyre::BackendError> {
213        let Some(max_retained_bytes) = self.max_retained_bytes else {
214            return Ok(());
215        };
216        while !self.entries.is_empty()
217            && self
218                .retained_bytes
219                .checked_add(incoming_bytes)
220                .is_none_or(|total| total > max_retained_bytes)
221        {
222            let Some(key) = self.pop_lru_key() else {
223                return Ok(());
224            };
225            let entry = self
226                .entries
227                .remove(&key)
228                .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache eviction key disappeared before removal; rebuild the resident graph cache because this indicates internal cache state corruption."))?;
229            self.retained_bytes = self
230                .retained_bytes
231                .checked_sub(entry.retained_bytes)
232                .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache retained byte accounting underflowed during eviction; rebuild the resident graph cache before reuse."))?;
233            self.evictions = self
234                .evictions
235                .checked_add(1)
236                .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache eviction counter overflowed u64; rebuild the resident graph cache before reuse."))?;
237            entry.graph.free(backend)?;
238        }
239        Ok(())
240    }
241
242    /// Free every resident graph owned by this cache.
243    pub fn free_all(&mut self, backend: &dyn vyre::VyreBackend) -> Result<(), vyre::BackendError> {
244        let mut first_error = None;
245        for (_, entry) in self.entries.drain() {
246            self.retained_bytes =
247                self.retained_bytes
248                    .checked_sub(entry.retained_bytes)
249                    .ok_or_else(|| vyre::BackendError::InvalidProgram {
250                        fix: "Fix: resident graph cache retained byte accounting underflowed while freeing all entries; rebuild the cache before reuse.".to_string(),
251                    })?;
252            if let Err(error) = entry.graph.free(backend) {
253                first_error.get_or_insert(error);
254            }
255        }
256        self.lru.clear();
257        if let Some(error) = first_error {
258            Err(error)
259        } else {
260            Ok(())
261        }
262    }
263
264    fn next_cache_tick(&mut self) -> Result<u64, vyre::BackendError> {
265        self.clock = self
266            .clock
267            .checked_add(1)
268            .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache logical clock overflowed u64; rebuild the resident graph cache before reuse."))?;
269        Ok(self.clock)
270    }
271
272    fn pop_lru_key(&mut self) -> Option<ResidentGraphCacheIdentity> {
273        self.lru.pop_valid(
274            |key| self.entries.get(key).map(|entry| entry.last_seen),
275            || self.entries.keys().next().cloned(),
276        )
277    }
278
279    fn record_lru(
280        &mut self,
281        key: ResidentGraphCacheIdentity,
282        last_seen: u64,
283    ) -> Result<(), vyre::BackendError> {
284        self.lru
285            .record(
286                key,
287                last_seen,
288                self.entries
289                    .iter()
290                    .map(|(key, entry)| (key.clone(), entry.last_seen)),
291                "resident graph cache",
292            )
293            .map_err(resident_graph_cache_error)
294    }
295
296    #[cfg(test)]
297    pub(super) fn lru_len_for_tests(&self) -> usize {
298        self.lru.len()
299    }
300}
301
302fn resident_graph_cache_error(fix: impl Into<String>) -> vyre::BackendError {
303    vyre::BackendError::InvalidProgram { fix: fix.into() }
304}