1use amaters_core::compute::Circuit;
24use amaters_core::types::Predicate;
25use parking_lot::Mutex;
26use std::collections::{HashMap, VecDeque};
27use std::sync::Arc;
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct CircuitCacheKey([u8; 32]);
36
37impl CircuitCacheKey {
38 pub fn from_predicate(predicate: &Predicate) -> Self {
43 let repr = format!("{predicate:?}");
44 let hash = blake3::hash(repr.as_bytes());
45 Self(*hash.as_bytes())
46 }
47
48 pub fn as_bytes(&self) -> &[u8; 32] {
50 &self.0
51 }
52}
53
54#[derive(Debug, Clone, Default)]
60pub struct CircuitCacheStats {
61 pub hits: u64,
63 pub misses: u64,
65 pub evictions: u64,
67 pub current_size: usize,
69}
70
71impl CircuitCacheStats {
72 pub fn hit_rate(&self) -> f64 {
76 let total = self.hits + self.misses;
77 if total == 0 {
78 0.0
79 } else {
80 self.hits as f64 / total as f64
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
91pub struct CircuitCacheConfig {
92 pub max_entries: usize,
97}
98
99impl Default for CircuitCacheConfig {
100 fn default() -> Self {
101 Self { max_entries: 256 }
102 }
103}
104
105impl CircuitCacheConfig {
106 pub fn new(max_entries: usize) -> Self {
108 Self { max_entries }
109 }
110}
111
112struct CacheInner {
120 map: HashMap<CircuitCacheKey, Circuit>,
122 order: VecDeque<CircuitCacheKey>,
124 stats: CircuitCacheStats,
126 config: CircuitCacheConfig,
128}
129
130impl CacheInner {
131 fn new(config: CircuitCacheConfig) -> Self {
132 let capacity = config.max_entries;
133 Self {
134 map: HashMap::with_capacity(capacity),
135 order: VecDeque::with_capacity(capacity),
136 stats: CircuitCacheStats::default(),
137 config,
138 }
139 }
140
141 fn get(&mut self, key: &CircuitCacheKey) -> Option<Circuit> {
146 if let Some(circuit) = self.map.get(key).cloned() {
147 self.stats.hits += 1;
148 if let Some(pos) = self.order.iter().position(|k| k == key) {
150 self.order.remove(pos);
151 self.order.push_back(key.clone());
152 }
153 Some(circuit)
154 } else {
155 self.stats.misses += 1;
156 None
157 }
158 }
159
160 fn insert(&mut self, key: CircuitCacheKey, circuit: Circuit) {
167 if self.map.contains_key(&key) {
168 return; }
170 while self.map.len() >= self.config.max_entries {
172 if let Some(lru_key) = self.order.pop_front() {
173 self.map.remove(&lru_key);
174 self.stats.evictions += 1;
175 } else {
176 break; }
178 }
179 self.order.push_back(key.clone());
180 self.map.insert(key, circuit);
181 self.stats.current_size = self.map.len();
182 }
183
184 fn stats_snapshot(&self) -> CircuitCacheStats {
186 CircuitCacheStats {
187 current_size: self.map.len(),
188 ..self.stats.clone()
189 }
190 }
191}
192
193#[derive(Clone)]
212pub struct CircuitCache {
213 inner: Arc<Mutex<CacheInner>>,
214}
215
216impl std::fmt::Debug for CircuitCache {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 let stats = self.inner.lock().stats_snapshot();
219 f.debug_struct("CircuitCache")
220 .field("current_size", &stats.current_size)
221 .field("hits", &stats.hits)
222 .field("misses", &stats.misses)
223 .field("evictions", &stats.evictions)
224 .finish()
225 }
226}
227
228impl CircuitCache {
229 pub fn new(config: CircuitCacheConfig) -> Self {
231 Self {
232 inner: Arc::new(Mutex::new(CacheInner::new(config))),
233 }
234 }
235
236 pub fn get(&self, key: &CircuitCacheKey) -> Option<Circuit> {
240 self.inner.lock().get(key)
241 }
242
243 pub fn insert(&self, key: CircuitCacheKey, circuit: Circuit) {
247 self.inner.lock().insert(key, circuit);
248 }
249
250 pub fn get_or_compile<F>(
261 &self,
262 predicate: &Predicate,
263 compile_fn: F,
264 ) -> amaters_core::error::Result<Circuit>
265 where
266 F: FnOnce() -> amaters_core::error::Result<Circuit>,
267 {
268 let key = CircuitCacheKey::from_predicate(predicate);
269
270 if let Some(cached) = self.get(&key) {
272 return Ok(cached);
273 }
274
275 let circuit = compile_fn()?;
277 self.insert(key, circuit.clone());
278 Ok(circuit)
279 }
280
281 pub fn stats(&self) -> CircuitCacheStats {
283 self.inner.lock().stats_snapshot()
284 }
285
286 pub fn len(&self) -> usize {
288 self.inner.lock().map.len()
289 }
290
291 pub fn is_empty(&self) -> bool {
293 self.len() == 0
294 }
295}
296
297#[cfg(test)]
302mod tests {
303 use super::*;
304 use amaters_core::compute::{CircuitBuilder, EncryptedType};
305 use amaters_core::types::{CipherBlob, ColumnRef, Predicate};
306
307 fn make_simple_circuit() -> Circuit {
309 let mut builder = CircuitBuilder::new();
310 builder.declare_variable("x", EncryptedType::U8);
311 let x = builder.load("x");
312 builder.build(x).expect("simple circuit should build")
313 }
314
315 fn make_predicate() -> Predicate {
317 Predicate::Eq(
318 ColumnRef::new("col".to_string()),
319 CipherBlob::new(b"test".to_vec()),
320 )
321 }
322
323 #[test]
325 fn test_cache_miss_then_hit() {
326 let cache = CircuitCache::new(CircuitCacheConfig::default());
327 let pred = make_predicate();
328 let key = CircuitCacheKey::from_predicate(&pred);
329
330 assert!(cache.get(&key).is_none());
332 assert_eq!(cache.stats().misses, 1);
333 assert_eq!(cache.stats().hits, 0);
334
335 cache.insert(key.clone(), make_simple_circuit());
337
338 assert!(cache.get(&key).is_some());
340 assert_eq!(cache.stats().hits, 1);
341 }
342
343 #[test]
345 fn test_eviction_at_capacity() {
346 let cache = CircuitCache::new(CircuitCacheConfig::new(2));
347
348 let preds: Vec<Predicate> = (0u8..3)
349 .map(|i| Predicate::Eq(ColumnRef::new(format!("col{i}")), CipherBlob::new(vec![i])))
350 .collect();
351
352 for pred in &preds[..2] {
354 let k = CircuitCacheKey::from_predicate(pred);
355 cache.insert(k, make_simple_circuit());
356 }
357 assert_eq!(cache.len(), 2);
358
359 let key3 = CircuitCacheKey::from_predicate(&preds[2]);
361 cache.insert(key3, make_simple_circuit());
362 assert_eq!(cache.len(), 2);
363 assert_eq!(cache.stats().evictions, 1);
364
365 let key0 = CircuitCacheKey::from_predicate(&preds[0]);
367 assert!(
368 cache.get(&key0).is_none(),
369 "LRU entry should have been evicted"
370 );
371 }
372
373 #[test]
375 fn test_get_or_compile_hit_skips_compile_fn() {
376 let cache = CircuitCache::new(CircuitCacheConfig::default());
377 let pred = make_predicate();
378 let key = CircuitCacheKey::from_predicate(&pred);
379 cache.insert(key, make_simple_circuit());
380
381 let mut called = false;
382 let result = cache.get_or_compile(&pred, || {
383 called = true;
384 Ok(make_simple_circuit())
385 });
386 assert!(result.is_ok(), "get_or_compile should succeed");
387 assert!(!called, "compile_fn must not be called on a cache hit");
388 }
389
390 #[test]
392 fn test_get_or_compile_miss_calls_compile_fn_and_caches() {
393 let cache = CircuitCache::new(CircuitCacheConfig::default());
394 let pred = make_predicate();
395
396 let mut call_count = 0u32;
397 let result = cache.get_or_compile(&pred, || {
398 call_count += 1;
399 Ok(make_simple_circuit())
400 });
401 assert!(result.is_ok());
402 assert_eq!(call_count, 1, "compile_fn should be called exactly once");
403
404 let result2 = cache.get_or_compile(&pred, || {
406 call_count += 1;
407 Ok(make_simple_circuit())
408 });
409 assert!(result2.is_ok());
410 assert_eq!(
411 call_count, 1,
412 "compile_fn must not be called again on a subsequent hit"
413 );
414 }
415
416 #[test]
418 fn test_different_predicates_different_keys() {
419 let p1 = Predicate::Eq(ColumnRef::new("a".to_string()), CipherBlob::new(vec![1]));
420 let p2 = Predicate::Eq(ColumnRef::new("b".to_string()), CipherBlob::new(vec![2]));
421 let k1 = CircuitCacheKey::from_predicate(&p1);
422 let k2 = CircuitCacheKey::from_predicate(&p2);
423 assert_ne!(k1, k2, "distinct predicates must map to distinct keys");
424 }
425
426 #[test]
428 fn test_same_predicate_same_key() {
429 let pred = make_predicate();
430 let k1 = CircuitCacheKey::from_predicate(&pred);
431 let k2 = CircuitCacheKey::from_predicate(&pred);
432 assert_eq!(k1, k2, "same predicate must always map to the same key");
433 }
434
435 #[test]
437 fn test_clone_shares_underlying_cache() {
438 let cache1 = CircuitCache::new(CircuitCacheConfig::default());
439 let cache2 = cache1.clone();
440 let pred = make_predicate();
441 let key = CircuitCacheKey::from_predicate(&pred);
442 cache1.insert(key.clone(), make_simple_circuit());
443 assert!(
444 cache2.get(&key).is_some(),
445 "cloned handle must see entries inserted via the original handle"
446 );
447 }
448
449 #[test]
451 fn test_hit_rate_zero_when_no_lookups() {
452 let cache = CircuitCache::new(CircuitCacheConfig::default());
453 let stats = cache.stats();
454 assert!(
455 (stats.hit_rate() - 0.0).abs() < f64::EPSILON,
456 "hit_rate should be 0.0 with no lookups"
457 );
458 }
459
460 #[test]
462 fn test_hit_rate_accuracy() {
463 let cache = CircuitCache::new(CircuitCacheConfig::default());
464 let pred = make_predicate();
465 let key = CircuitCacheKey::from_predicate(&pred);
466 cache.insert(key.clone(), make_simple_circuit());
467
468 for _ in 0..3 {
470 let _ = cache.get(&key);
471 }
472 let other = Predicate::Gt(ColumnRef::new("z".to_string()), CipherBlob::new(vec![9]));
474 let _ = cache.get(&CircuitCacheKey::from_predicate(&other));
475
476 let stats = cache.stats();
477 assert!(
479 (stats.hit_rate() - 0.75).abs() < 1e-9,
480 "expected hit_rate 0.75, got {}",
481 stats.hit_rate()
482 );
483 }
484
485 #[test]
487 fn test_idempotent_insert() {
488 let cache = CircuitCache::new(CircuitCacheConfig::default());
489 let pred = make_predicate();
490 let key = CircuitCacheKey::from_predicate(&pred);
491
492 cache.insert(key.clone(), make_simple_circuit());
493 cache.insert(key.clone(), make_simple_circuit()); assert_eq!(cache.len(), 1, "duplicate insert must not grow the cache");
495 }
496
497 #[test]
499 fn test_lru_promotion_protects_from_eviction() {
500 let cache = CircuitCache::new(CircuitCacheConfig::new(2));
501
502 let pred_a = Predicate::Eq(ColumnRef::new("a".to_string()), CipherBlob::new(vec![0]));
503 let pred_b = Predicate::Gt(ColumnRef::new("b".to_string()), CipherBlob::new(vec![1]));
504 let pred_c = Predicate::Lt(ColumnRef::new("c".to_string()), CipherBlob::new(vec![2]));
505
506 let key_a = CircuitCacheKey::from_predicate(&pred_a);
507 let key_b = CircuitCacheKey::from_predicate(&pred_b);
508 let key_c = CircuitCacheKey::from_predicate(&pred_c);
509
510 cache.insert(key_a.clone(), make_simple_circuit());
512 cache.insert(key_b.clone(), make_simple_circuit());
513
514 let _ = cache.get(&key_a);
516
517 cache.insert(key_c.clone(), make_simple_circuit());
519
520 assert!(cache.get(&key_a).is_some(), "A should survive (promoted)");
521 assert!(cache.get(&key_b).is_none(), "B should be evicted (LRU)");
522 assert!(cache.get(&key_c).is_some(), "C was just inserted");
523 }
524
525 #[test]
527 fn test_debug_format() {
528 let cache = CircuitCache::new(CircuitCacheConfig::default());
529 let dbg = format!("{:?}", cache);
530 assert!(
531 dbg.contains("CircuitCache"),
532 "debug string should name the type"
533 );
534 }
535}