json_eval_rs/
eval_cache.rs

1#[cfg(feature = "parallel")]
2use dashmap::DashMap;
3#[cfg(not(feature = "parallel"))]
4use std::cell::RefCell;
5#[cfg(not(feature = "parallel"))]
6use std::collections::HashMap;
7
8use ahash::AHasher;
9use indexmap::IndexSet;
10use serde_json::Value;
11use std::hash::{Hash, Hasher};
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::Arc;
14
15/// Fast hash computation for cache keys
16/// Uses AHash for performance and FxHash-like quality
17#[inline]
18fn compute_hash<T: Hash>(value: &T) -> u64 {
19    let mut hasher = AHasher::default();
20    value.hash(&mut hasher);
21    hasher.finish()
22}
23
24/// Cache key: combines evaluation logic ID with hash of all dependent values
25/// Zero-copy design: stores references to logic and dependency paths
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub struct CacheKey {
28    /// Evaluation key (e.g., "$params.foo")
29    pub eval_key: String,
30    /// Single hash of all dependency values combined (for efficiency)
31    pub deps_hash: u64,
32}
33
34impl CacheKey {
35    /// Create cache key from evaluation key and dependency values
36    /// Dependencies are pre-filtered by caller (JSONEval)
37    /// Hashes all dependency values together in one pass for efficiency
38    pub fn new(
39        eval_key: String,
40        dependencies: &IndexSet<String>,
41        values: &[(String, &Value)],
42    ) -> Self {
43        // Build hash map for fast lookup
44        let value_map: std::collections::HashMap<&str, &Value> =
45            values.iter().map(|(k, v)| (k.as_str(), *v)).collect();
46
47        // Combine all dependency values into a single string for hashing
48        // This is more efficient than hashing each value separately
49        let mut combined = String::new();
50        for dep_key in dependencies.iter() {
51            combined.push_str(dep_key);
52            combined.push(':');
53            if let Some(value) = value_map.get(dep_key.as_str()) {
54                combined.push_str(&value.to_string());
55            } else {
56                combined.push_str("null");
57            }
58            combined.push(';');
59        }
60
61        // Compute single hash for all dependencies
62        let deps_hash = compute_hash(&combined);
63
64        Self {
65            eval_key,
66            deps_hash,
67        }
68    }
69
70    /// Create a simple cache key without dependencies (for evaluations with no deps)
71    pub fn simple(eval_key: String) -> Self {
72        Self {
73            eval_key,
74            deps_hash: 0, // No dependencies = hash of 0
75        }
76    }
77}
78
79/// Zero-copy cache store
80/// With parallel feature: Uses DashMap for thread-safe concurrent access
81/// Without parallel feature: Uses HashMap + RefCell for ultra-fast single-threaded access
82/// Values are stored behind Arc to enable cheap cloning
83pub struct EvalCache {
84    #[cfg(feature = "parallel")]
85    /// Cache storage: DashMap for thread-safe concurrent access
86    cache: DashMap<CacheKey, Arc<Value>>,
87
88    #[cfg(not(feature = "parallel"))]
89    /// Cache storage: HashMap + RefCell for ultra-fast single-threaded access
90    cache: RefCell<HashMap<CacheKey, Arc<Value>>>,
91
92    /// Cache statistics (atomic for thread safety)
93    hits: AtomicUsize,
94    misses: AtomicUsize,
95}
96
97impl EvalCache {
98    /// Create a new empty cache
99    pub fn new() -> Self {
100        Self {
101            #[cfg(feature = "parallel")]
102            cache: DashMap::new(),
103            #[cfg(not(feature = "parallel"))]
104            cache: RefCell::new(HashMap::new()),
105            hits: AtomicUsize::new(0),
106            misses: AtomicUsize::new(0),
107        }
108    }
109
110    /// Create cache with preallocated capacity
111    pub fn with_capacity(capacity: usize) -> Self {
112        Self {
113            #[cfg(feature = "parallel")]
114            cache: DashMap::with_capacity(capacity),
115            #[cfg(not(feature = "parallel"))]
116            cache: RefCell::new(HashMap::with_capacity(capacity)),
117            hits: AtomicUsize::new(0),
118            misses: AtomicUsize::new(0),
119        }
120    }
121
122    /// Get cached result (zero-copy via Arc clone)
123    /// Returns None if not cached
124    #[cfg(feature = "parallel")]
125    /// Thread-safe: can be called concurrently
126    #[inline]
127    pub fn get(&self, key: &CacheKey) -> Option<Arc<Value>> {
128        if let Some(value) = self.cache.get(key) {
129            self.hits.fetch_add(1, Ordering::Relaxed);
130            Some(Arc::clone(value.value()))
131        } else {
132            self.misses.fetch_add(1, Ordering::Relaxed);
133            None
134        }
135    }
136
137    /// Get cached result (zero-copy via Arc clone)
138    /// Returns None if not cached
139    #[cfg(not(feature = "parallel"))]
140    /// Ultra-fast single-threaded access
141    #[inline]
142    pub fn get(&self, key: &CacheKey) -> Option<Arc<Value>> {
143        if let Some(value) = self.cache.borrow().get(key) {
144            self.hits.fetch_add(1, Ordering::Relaxed);
145            Some(Arc::clone(value))
146        } else {
147            self.misses.fetch_add(1, Ordering::Relaxed);
148            None
149        }
150    }
151
152    /// Insert result into cache (wraps in Arc for zero-copy sharing)
153    #[cfg(feature = "parallel")]
154    /// Thread-safe: can be called concurrently
155    #[inline]
156    pub fn insert(&self, key: CacheKey, value: Value) {
157        self.cache.insert(key, Arc::new(value));
158    }
159
160    /// Insert result into cache (wraps in Arc for zero-copy sharing)
161    #[cfg(not(feature = "parallel"))]
162    /// Ultra-fast single-threaded access
163    #[inline]
164    pub fn insert(&self, key: CacheKey, value: Value) {
165        self.cache.borrow_mut().insert(key, Arc::new(value));
166    }
167
168    /// Insert with Arc-wrapped value (zero-copy if already Arc)
169    #[cfg(feature = "parallel")]
170    /// Thread-safe: can be called concurrently
171    #[inline]
172    pub fn insert_arc(&self, key: CacheKey, value: Arc<Value>) {
173        self.cache.insert(key, value);
174    }
175
176    /// Insert with Arc-wrapped value (zero-copy if already Arc)
177    #[cfg(not(feature = "parallel"))]
178    /// Ultra-fast single-threaded access
179    #[inline]
180    pub fn insert_arc(&self, key: CacheKey, value: Arc<Value>) {
181        self.cache.borrow_mut().insert(key, value);
182    }
183
184    /// Clear all cached entries
185    #[cfg(feature = "parallel")]
186    pub fn clear(&self) {
187        self.cache.clear();
188        self.hits.store(0, Ordering::Relaxed);
189        self.misses.store(0, Ordering::Relaxed);
190    }
191
192    /// Clear all cached entries
193    #[cfg(not(feature = "parallel"))]
194    pub fn clear(&self) {
195        self.cache.borrow_mut().clear();
196        self.hits.store(0, Ordering::Relaxed);
197        self.misses.store(0, Ordering::Relaxed);
198    }
199
200    /// Get cache hit rate (0.0 to 1.0)
201    pub fn hit_rate(&self) -> f64 {
202        let hits = self.hits.load(Ordering::Relaxed);
203        let misses = self.misses.load(Ordering::Relaxed);
204        let total = hits + misses;
205        if total == 0 {
206            0.0
207        } else {
208            hits as f64 / total as f64
209        }
210    }
211
212    /// Get cache statistics
213    #[cfg(feature = "parallel")]
214    pub fn stats(&self) -> CacheStats {
215        CacheStats {
216            hits: self.hits.load(Ordering::Relaxed),
217            misses: self.misses.load(Ordering::Relaxed),
218            entries: self.cache.len(),
219            hit_rate: self.hit_rate(),
220        }
221    }
222
223    /// Get cache statistics
224    #[cfg(not(feature = "parallel"))]
225    pub fn stats(&self) -> CacheStats {
226        CacheStats {
227            hits: self.hits.load(Ordering::Relaxed),
228            misses: self.misses.load(Ordering::Relaxed),
229            entries: self.cache.borrow().len(),
230            hit_rate: self.hit_rate(),
231        }
232    }
233
234    /// Get number of cached entries
235    #[cfg(feature = "parallel")]
236    #[inline]
237    pub fn len(&self) -> usize {
238        self.cache.len()
239    }
240
241    /// Get number of cached entries
242    #[cfg(not(feature = "parallel"))]
243    #[inline]
244    pub fn len(&self) -> usize {
245        self.cache.borrow().len()
246    }
247
248    /// Check if cache is empty
249    #[cfg(feature = "parallel")]
250    #[inline]
251    pub fn is_empty(&self) -> bool {
252        self.cache.is_empty()
253    }
254
255    /// Check if cache is empty
256    #[cfg(not(feature = "parallel"))]
257    #[inline]
258    pub fn is_empty(&self) -> bool {
259        self.cache.borrow().is_empty()
260    }
261
262    /// Remove specific entry
263    #[cfg(feature = "parallel")]
264    #[inline]
265    pub fn remove(&self, key: &CacheKey) -> Option<Arc<Value>> {
266        self.cache.remove(key).map(|(_, v)| v)
267    }
268
269    /// Remove specific entry
270    #[cfg(not(feature = "parallel"))]
271    #[inline]
272    pub fn remove(&self, key: &CacheKey) -> Option<Arc<Value>> {
273        self.cache.borrow_mut().remove(key)
274    }
275
276    /// Remove entries based on a predicate function
277    /// Predicate returns true to keep the entry, false to remove it
278    #[cfg(feature = "parallel")]
279    pub fn retain<F>(&self, predicate: F)
280    where
281        F: Fn(&CacheKey, &Arc<Value>) -> bool,
282    {
283        self.cache.retain(|k, v| predicate(k, v));
284    }
285
286    /// Remove entries based on a predicate function
287    /// Predicate returns true to keep the entry, false to remove it
288    #[cfg(not(feature = "parallel"))]
289    pub fn retain<F>(&self, predicate: F)
290    where
291        F: Fn(&CacheKey, &Arc<Value>) -> bool,
292    {
293        self.cache.borrow_mut().retain(|k, v| predicate(k, v));
294    }
295
296    /// Invalidate cache entries that depend on changed paths
297    /// Efficiently removes only affected entries
298    #[cfg(feature = "parallel")]
299    pub fn invalidate_dependencies(&self, changed_paths: &[String]) {
300        // Build a set of changed path hashes for fast lookup
301        let changed_hashes: IndexSet<String> = changed_paths.iter().cloned().collect();
302
303        // Remove cache entries whose eval_key is in the changed set
304        self.cache
305            .retain(|cache_key, _| !changed_hashes.contains(&cache_key.eval_key));
306    }
307
308    /// Invalidate cache entries that depend on changed paths
309    /// Efficiently removes only affected entries
310    #[cfg(not(feature = "parallel"))]
311    pub fn invalidate_dependencies(&self, changed_paths: &[String]) {
312        // Build a set of changed path hashes for fast lookup
313        let changed_hashes: IndexSet<String> = changed_paths.iter().cloned().collect();
314
315        // Remove cache entries whose eval_key is in the changed set
316        self.cache
317            .borrow_mut()
318            .retain(|cache_key, _| !changed_hashes.contains(&cache_key.eval_key));
319    }
320}
321
322impl Default for EvalCache {
323    fn default() -> Self {
324        Self::new()
325    }
326}
327
328/// Cache statistics
329#[derive(Debug, Clone, Copy)]
330pub struct CacheStats {
331    pub hits: usize,
332    pub misses: usize,
333    pub entries: usize,
334    pub hit_rate: f64,
335}
336
337impl std::fmt::Display for CacheStats {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        write!(
340            f,
341            "Cache Stats: {} entries, {} hits, {} misses, {:.2}% hit rate",
342            self.entries,
343            self.hits,
344            self.misses,
345            self.hit_rate * 100.0
346        )
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use serde_json::json;
354
355    #[test]
356    fn test_cache_key_creation() {
357        let eval_key = "$params.foo".to_string();
358        let mut deps = IndexSet::new();
359        deps.insert("$params.bar".to_string());
360        deps.insert("data.value".to_string());
361
362        let val1 = json!(42);
363        let val2 = json!("test");
364        let values = vec![
365            ("$params.bar".to_string(), &val1),
366            ("data.value".to_string(), &val2),
367        ];
368
369        let key1 = CacheKey::new(eval_key.clone(), &deps, &values);
370        let key2 = CacheKey::new(eval_key.clone(), &deps, &values);
371
372        // Same inputs should produce same key
373        assert_eq!(key1, key2);
374    }
375
376    #[test]
377    fn test_cache_key_different_values() {
378        let eval_key = "$params.foo".to_string();
379        let mut deps = IndexSet::new();
380        deps.insert("data.value".to_string());
381
382        let val1 = json!(42);
383        let val2 = json!(43);
384        let values1 = vec![("data.value".to_string(), &val1)];
385        let values2 = vec![("data.value".to_string(), &val2)];
386
387        let key1 = CacheKey::new(eval_key.clone(), &deps, &values1);
388        let key2 = CacheKey::new(eval_key.clone(), &deps, &values2);
389
390        // Different values should produce different keys
391        assert_ne!(key1, key2);
392    }
393
394    #[test]
395    fn test_cache_operations() {
396        let cache = EvalCache::new();
397
398        let key = CacheKey::simple("test".to_string());
399        let value = json!({"result": 42});
400
401        // Test miss
402        assert!(cache.get(&key).is_none());
403        assert_eq!(cache.stats().misses, 1);
404
405        // Insert and test hit
406        cache.insert(key.clone(), value.clone());
407        assert_eq!(cache.get(&key).unwrap().as_ref(), &value);
408        assert_eq!(cache.stats().hits, 1);
409
410        // Test stats
411        let stats = cache.stats();
412        assert_eq!(stats.entries, 1);
413        assert_eq!(stats.hit_rate, 0.5); // 1 hit, 1 miss
414    }
415
416    #[test]
417    fn test_cache_clear() {
418        let cache = EvalCache::new();
419        cache.insert(CacheKey::simple("test".to_string()), json!(42));
420
421        assert_eq!(cache.len(), 1);
422        cache.clear();
423        assert_eq!(cache.len(), 0);
424        assert_eq!(cache.stats().hits, 0);
425    }
426
427    #[test]
428    fn test_invalidate_dependencies() {
429        let cache = EvalCache::new();
430
431        // Add cache entries
432        cache.insert(CacheKey::simple("$params.foo".to_string()), json!(1));
433        cache.insert(CacheKey::simple("$params.bar".to_string()), json!(2));
434        cache.insert(CacheKey::simple("$params.baz".to_string()), json!(3));
435
436        assert_eq!(cache.len(), 3);
437
438        // Invalidate one path
439        cache.invalidate_dependencies(&["$params.foo".to_string()]);
440
441        assert_eq!(cache.len(), 2);
442        assert!(cache
443            .get(&CacheKey::simple("$params.foo".to_string()))
444            .is_none());
445        assert!(cache
446            .get(&CacheKey::simple("$params.bar".to_string()))
447            .is_some());
448    }
449}