1use crate::fixed_point_graph::FixedPointAnalysisKind;
2use std::cmp::Ordering;
3
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub enum ResidentGraphCacheDomain {
8 IfdsCsr,
10 FixedPoint(FixedPointAnalysisKind),
12}
13
14impl ResidentGraphCacheDomain {
15 #[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct ResidentGraphCacheIdentity {
38 pub domain: ResidentGraphCacheDomain,
40 pub backend_id: &'static str,
42 pub backend_version: &'static str,
44 pub layout_hash: u64,
46 pub node_count: u32,
48 pub edge_count: u32,
50 pub frontier_words: u32,
52}
53
54impl ResidentGraphCacheIdentity {
55 #[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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
155pub struct ResidentGraphCacheMissEvidence {
156 pub total_entries: usize,
158 pub same_domain_entries: usize,
160 pub same_backend_entries: usize,
162 pub same_layout_entries: usize,
164 pub same_shape_entries: usize,
167}
168
169impl ResidentGraphCacheMissEvidence {
170 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum ResidentGraphCacheMissReason {
205 EmptyCache,
207 DomainChanged,
209 BackendChanged,
211 LayoutChanged,
213 ShapeChanged,
215 KeyAbsent,
217}
218
219impl ResidentGraphCacheMissReason {
220 #[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 #[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}