Skip to main content

weir/
resident_cache_identity.rs

1use crate::fixed_point_graph::FixedPointAnalysisKind;
2use std::cmp::Ordering;
3
4/// Resident graph cache domain participating in shared cache identity.
5#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub enum ResidentGraphCacheDomain {
8    /// Prepared IFDS exploded-supergraph CSR buffers.
9    IfdsCsr,
10    /// Fixed-point forward graph buffers for one analysis family.
11    FixedPoint(FixedPointAnalysisKind),
12}
13
14impl ResidentGraphCacheDomain {
15    /// Stable numeric code used by deterministic evidence ordering and digests.
16    #[must_use]
17    pub const fn code(self) -> u8 {
18        match self {
19            Self::IfdsCsr => 1,
20            Self::FixedPoint(FixedPointAnalysisKind::Generic) => 16,
21            Self::FixedPoint(FixedPointAnalysisKind::Reaching) => 17,
22            Self::FixedPoint(FixedPointAnalysisKind::Live) => 18,
23            Self::FixedPoint(FixedPointAnalysisKind::PointsToSubset) => 19,
24            Self::FixedPoint(FixedPointAnalysisKind::Slice) => 20,
25        }
26    }
27}
28
29/// Backend-neutral resident graph cache identity shared by IFDS and
30/// fixed-point resident caches.
31///
32/// The tuple is intentionally not just `layout_hash`: resident handles are
33/// backend-owned, backend-version-sensitive resources, and solver scratch
34/// compatibility depends on the shape carried beside the layout hash.
35#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct ResidentGraphCacheIdentity {
38    /// Resident cache domain and analysis family.
39    pub domain: ResidentGraphCacheDomain,
40    /// Stable backend/device family id.
41    pub backend_id: &'static str,
42    /// Stable backend implementation version.
43    pub backend_version: &'static str,
44    /// Deterministic graph layout/content hash.
45    pub layout_hash: u64,
46    /// Graph node count used by resident solver scratch.
47    pub node_count: u32,
48    /// Graph edge count used by resident uploads and diagnostics.
49    pub edge_count: u32,
50    /// Frontier bitset word count required by the resident solve.
51    pub frontier_words: u32,
52}
53
54impl ResidentGraphCacheIdentity {
55    /// Identity for a prepared IFDS resident CSR.
56    #[must_use]
57    pub const fn ifds_csr(
58        backend_id: &'static str,
59        backend_version: &'static str,
60        layout_hash: u64,
61        node_count: u32,
62        edge_count: u32,
63        frontier_words: u32,
64    ) -> Self {
65        Self {
66            domain: ResidentGraphCacheDomain::IfdsCsr,
67            backend_id,
68            backend_version,
69            layout_hash,
70            node_count,
71            edge_count,
72            frontier_words,
73        }
74    }
75
76    /// Identity for a fixed-point resident graph.
77    #[must_use]
78    pub const fn fixed_point(
79        backend_id: &'static str,
80        backend_version: &'static str,
81        analysis_kind: FixedPointAnalysisKind,
82        layout_hash: u64,
83        node_count: u32,
84        edge_count: u32,
85        frontier_words: u32,
86    ) -> Self {
87        Self {
88            domain: ResidentGraphCacheDomain::FixedPoint(analysis_kind),
89            backend_id,
90            backend_version,
91            layout_hash,
92            node_count,
93            edge_count,
94            frontier_words,
95        }
96    }
97
98    /// Tuple-boundary-preserving 64-bit digest for compact release evidence.
99    #[must_use]
100    pub fn stable_digest64(&self) -> u64 {
101        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
102        const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
103
104        fn mix(mut hash: u64, bytes: &[u8]) -> u64 {
105            for byte in bytes {
106                hash ^= u64::from(*byte);
107                hash = hash.wrapping_mul(FNV_PRIME);
108            }
109            hash
110        }
111
112        let mut hash = FNV_OFFSET;
113        hash = mix(hash, b"weir-resident-graph-cache-identity-v1\0domain\0");
114        hash = mix(hash, &[self.domain.code()]);
115        hash = mix(hash, b"\0backend-id\0");
116        hash = mix(hash, self.backend_id.as_bytes());
117        hash = mix(hash, b"\0backend-version\0");
118        hash = mix(hash, self.backend_version.as_bytes());
119        hash = mix(hash, b"\0layout\0");
120        hash = mix(hash, &self.layout_hash.to_le_bytes());
121        hash = mix(hash, b"\0nodes\0");
122        hash = mix(hash, &self.node_count.to_le_bytes());
123        hash = mix(hash, b"\0edges\0");
124        hash = mix(hash, &self.edge_count.to_le_bytes());
125        hash = mix(hash, b"\0frontier-words\0");
126        mix(hash, &self.frontier_words.to_le_bytes())
127    }
128
129    /// Deterministic ordering for cache snapshots and release evidence.
130    #[must_use]
131    pub fn cmp_stable(&self, other: &Self) -> Ordering {
132        (
133            self.domain.code(),
134            self.backend_id,
135            self.backend_version,
136            self.layout_hash,
137            self.node_count,
138            self.edge_count,
139            self.frontier_words,
140        )
141            .cmp(&(
142                other.domain.code(),
143                other.backend_id,
144                other.backend_version,
145                other.layout_hash,
146                other.node_count,
147                other.edge_count,
148                other.frontier_words,
149            ))
150    }
151}
152
153/// Adjacent cache identity evidence used to explain resident graph misses.
154#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
155pub struct ResidentGraphCacheMissEvidence {
156    /// Total resident graph cache entries inspected.
157    pub total_entries: usize,
158    /// Entries in the same resident graph domain.
159    pub same_domain_entries: usize,
160    /// Entries with the same domain, backend id, and backend version.
161    pub same_backend_entries: usize,
162    /// Entries with the same domain, backend identity, and layout hash.
163    pub same_layout_entries: usize,
164    /// Entries with matching domain, backend, layout, node, edge, and frontier
165    /// word counts.
166    pub same_shape_entries: usize,
167}
168
169impl ResidentGraphCacheMissEvidence {
170    /// Build miss evidence from cache identities adjacent to a requested key.
171    #[must_use]
172    pub fn from_identities<'a>(
173        cached: impl Iterator<Item = &'a ResidentGraphCacheIdentity>,
174        requested: &ResidentGraphCacheIdentity,
175    ) -> Self {
176        let mut evidence = Self::default();
177        for identity in cached {
178            evidence.total_entries += 1;
179            if identity.domain == requested.domain {
180                evidence.same_domain_entries += 1;
181                if identity.backend_id == requested.backend_id
182                    && identity.backend_version == requested.backend_version
183                {
184                    evidence.same_backend_entries += 1;
185                    if identity.layout_hash == requested.layout_hash {
186                        evidence.same_layout_entries += 1;
187                        if identity.node_count == requested.node_count
188                            && identity.edge_count == requested.edge_count
189                            && identity.frontier_words == requested.frontier_words
190                        {
191                            evidence.same_shape_entries += 1;
192                        }
193                    }
194                }
195            }
196        }
197        evidence
198    }
199}
200
201/// Backend-neutral resident cache-miss reason for diagnostics and release
202/// evidence.
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum ResidentGraphCacheMissReason {
205    /// The resident graph cache has no entries.
206    EmptyCache,
207    /// Entries exist but for a different resident graph domain.
208    DomainChanged,
209    /// The same domain exists but backend id or backend version changed.
210    BackendChanged,
211    /// Domain and backend match, but the graph layout/content hash changed.
212    LayoutChanged,
213    /// Domain, backend, and layout hash matched, but shape fields differ.
214    ShapeChanged,
215    /// Adjacent identity says the entry should exist, but the final key missed.
216    KeyAbsent,
217}
218
219impl ResidentGraphCacheMissReason {
220    /// Classify a resident graph cache miss from adjacent identity evidence.
221    #[must_use]
222    pub const fn classify(evidence: ResidentGraphCacheMissEvidence) -> Self {
223        if evidence.total_entries == 0 {
224            Self::EmptyCache
225        } else if evidence.same_domain_entries == 0 {
226            Self::DomainChanged
227        } else if evidence.same_backend_entries == 0 {
228            Self::BackendChanged
229        } else if evidence.same_layout_entries == 0 {
230            Self::LayoutChanged
231        } else if evidence.same_shape_entries == 0 {
232            Self::ShapeChanged
233        } else {
234            Self::KeyAbsent
235        }
236    }
237
238    /// Stable metric suffix for cache telemetry.
239    #[must_use]
240    pub const fn metric_suffix(self) -> &'static str {
241        match self {
242            Self::EmptyCache => "empty_cache",
243            Self::DomainChanged => "domain_changed",
244            Self::BackendChanged => "backend_changed",
245            Self::LayoutChanged => "layout_changed",
246            Self::ShapeChanged => "shape_changed",
247            Self::KeyAbsent => "key_absent",
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn ifds_identity() -> ResidentGraphCacheIdentity {
257        ResidentGraphCacheIdentity::ifds_csr("backend-a", "v1", 0x1111, 64, 8, 2)
258    }
259
260    #[test]
261    fn resident_graph_identity_digest_separates_cache_compatibility_fields() {
262        let base = ifds_identity();
263
264        assert_ne!(
265            base.stable_digest64(),
266            ResidentGraphCacheIdentity::ifds_csr("backend-b", "v1", 0x1111, 64, 8, 2)
267                .stable_digest64()
268        );
269        assert_ne!(
270            base.stable_digest64(),
271            ResidentGraphCacheIdentity::ifds_csr("backend-a", "v2", 0x1111, 64, 8, 2)
272                .stable_digest64()
273        );
274        assert_ne!(
275            base.stable_digest64(),
276            ResidentGraphCacheIdentity::ifds_csr("backend-a", "v1", 0x2222, 64, 8, 2)
277                .stable_digest64()
278        );
279        assert_ne!(
280            base.stable_digest64(),
281            ResidentGraphCacheIdentity::ifds_csr("backend-a", "v1", 0x1111, 64, 8, 3)
282                .stable_digest64()
283        );
284        assert_ne!(
285            base.stable_digest64(),
286            ResidentGraphCacheIdentity::fixed_point(
287                "backend-a",
288                "v1",
289                FixedPointAnalysisKind::Reaching,
290                0x1111,
291                64,
292                8,
293                2,
294            )
295            .stable_digest64()
296        );
297    }
298
299    #[test]
300    fn resident_graph_miss_reason_classifies_adjacent_identity() {
301        let requested = ifds_identity();
302        assert_eq!(
303            ResidentGraphCacheMissReason::classify(
304                ResidentGraphCacheMissEvidence::from_identities([].iter(), &requested)
305            ),
306            ResidentGraphCacheMissReason::EmptyCache
307        );
308
309        let different_domain = ResidentGraphCacheIdentity::fixed_point(
310            "backend-a",
311            "v1",
312            FixedPointAnalysisKind::Reaching,
313            0x1111,
314            64,
315            8,
316            2,
317        );
318        assert_eq!(
319            ResidentGraphCacheMissReason::classify(
320                ResidentGraphCacheMissEvidence::from_identities(
321                    std::iter::once(&different_domain),
322                    &requested,
323                )
324            ),
325            ResidentGraphCacheMissReason::DomainChanged
326        );
327
328        let different_backend =
329            ResidentGraphCacheIdentity::ifds_csr("backend-b", "v1", 0x1111, 64, 8, 2);
330        assert_eq!(
331            ResidentGraphCacheMissReason::classify(
332                ResidentGraphCacheMissEvidence::from_identities(
333                    std::iter::once(&different_backend),
334                    &requested,
335                )
336            ),
337            ResidentGraphCacheMissReason::BackendChanged
338        );
339
340        let different_layout =
341            ResidentGraphCacheIdentity::ifds_csr("backend-a", "v1", 0x2222, 64, 8, 2);
342        assert_eq!(
343            ResidentGraphCacheMissReason::classify(
344                ResidentGraphCacheMissEvidence::from_identities(
345                    std::iter::once(&different_layout),
346                    &requested,
347                )
348            ),
349            ResidentGraphCacheMissReason::LayoutChanged
350        );
351
352        let different_shape =
353            ResidentGraphCacheIdentity::ifds_csr("backend-a", "v1", 0x1111, 64, 8, 3);
354        assert_eq!(
355            ResidentGraphCacheMissReason::classify(
356                ResidentGraphCacheMissEvidence::from_identities(
357                    std::iter::once(&different_shape),
358                    &requested,
359                )
360            ),
361            ResidentGraphCacheMissReason::ShapeChanged
362        );
363
364        assert_eq!(
365            ResidentGraphCacheMissReason::classify(
366                ResidentGraphCacheMissEvidence::from_identities(std::iter::once(&requested), &requested)
367            ),
368            ResidentGraphCacheMissReason::KeyAbsent
369        );
370    }
371}