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