1use scirs2_core::numeric::Float;
6use std::collections::HashMap;
7use std::fmt::Debug;
8use std::time::SystemTime;
9
10use super::types::*;
11use crate::nas_engine::results::EvaluationResults;
12
13pub const DEFAULT_CACHE_CAPACITY: usize = 4096;
19
20#[derive(Debug)]
26pub struct EvaluationCache<T: Float + Debug + Send + Sync + 'static> {
27 evaluations: HashMap<String, CachedEvaluation<T>>,
29
30 metadata: CacheMetadata,
32
33 capacity: usize,
35
36 eviction_policy: CacheEvictionPolicy,
38
39 access_clock: u64,
43}
44
45#[derive(Debug, Clone)]
47pub struct CachedEvaluation<T: Float + Debug + Send + Sync + 'static> {
48 pub results: EvaluationResults<T>,
50
51 pub timestamp: SystemTime,
53
54 pub access_count: usize,
59
60 pub last_access_tick: u64,
62
63 pub is_valid: bool,
65}
66
67#[derive(Debug, Clone)]
69pub struct CacheMetadata {
70 pub total_entries: usize,
72
73 pub cache_size_bytes: usize,
75
76 pub last_cleanup: SystemTime,
78
79 pub version: String,
81}
82
83impl<T: Float + Debug + Default + Send + Sync> EvaluationCache<T> {
84 pub(crate) fn new() -> Self {
85 Self::with_capacity(DEFAULT_CACHE_CAPACITY, CacheEvictionPolicy::LRU)
86 }
87
88 pub fn with_capacity(capacity: usize, eviction_policy: CacheEvictionPolicy) -> Self {
93 Self {
94 evaluations: HashMap::new(),
95 metadata: CacheMetadata {
96 total_entries: 0,
97 cache_size_bytes: 0,
98 last_cleanup: SystemTime::now(),
99 version: env!("CARGO_PKG_VERSION").to_string(),
100 },
101 capacity: capacity.max(1),
102 eviction_policy,
103 access_clock: 0,
104 }
105 }
106
107 pub(crate) fn get(&mut self, key: &str) -> Option<&CachedEvaluation<T>> {
112 self.access_clock = self.access_clock.wrapping_add(1);
113 let tick = self.access_clock;
114
115 let entry = self.evaluations.get_mut(key)?;
116 if !entry.is_valid {
117 return None;
118 }
119 entry.access_count += 1;
120 entry.last_access_tick = tick;
121
122 self.evaluations.get(key)
123 }
124
125 pub(crate) fn insert(&mut self, key: String, results: EvaluationResults<T>) {
126 self.access_clock = self.access_clock.wrapping_add(1);
127
128 let cached_eval = CachedEvaluation {
129 results,
130 timestamp: SystemTime::now(),
131 access_count: 0,
132 last_access_tick: self.access_clock,
133 is_valid: true,
134 };
135
136 self.evaluations.insert(key, cached_eval);
137 self.evict_if_needed();
138 self.metadata.total_entries = self.evaluations.len();
139 }
140
141 fn evict_if_needed(&mut self) {
144 while self.evaluations.len() > self.capacity {
145 let victim = match self.eviction_policy {
146 CacheEvictionPolicy::LRU => self
148 .evaluations
149 .iter()
150 .min_by_key(|(_, v)| v.last_access_tick)
151 .map(|(k, _)| k.clone()),
152 CacheEvictionPolicy::LFU => self
154 .evaluations
155 .iter()
156 .min_by_key(|(_, v)| (v.access_count, v.last_access_tick))
157 .map(|(k, _)| k.clone()),
158 CacheEvictionPolicy::FIFO => self
160 .evaluations
161 .iter()
162 .min_by_key(|(_, v)| v.timestamp)
163 .map(|(k, _)| k.clone()),
164 CacheEvictionPolicy::Random => self.evaluations.keys().min().cloned(),
168 };
169
170 match victim {
171 Some(key) => {
172 self.evaluations.remove(&key);
173 }
174 None => break,
175 }
176 }
177
178 self.metadata.last_cleanup = SystemTime::now();
179 }
180
181 pub fn stats(&self) -> &CacheMetadata {
183 &self.metadata
184 }
185
186 pub fn capacity(&self) -> usize {
188 self.capacity
189 }
190
191 pub fn total_accesses(&self) -> usize {
193 self.evaluations.values().map(|v| v.access_count).sum()
194 }
195
196 pub fn access_frequency(&self, key: &str) -> Option<usize> {
207 self.evaluations.get(key).map(|entry| entry.access_count)
208 }
209
210 pub fn hottest_key(&self) -> Option<&str> {
213 self.evaluations
214 .iter()
215 .max_by(|(left_key, left), (right_key, right)| {
216 left.access_count
217 .cmp(&right.access_count)
218 .then_with(|| right_key.cmp(left_key))
219 })
220 .map(|(key, _)| key.as_str())
221 }
222
223 pub fn clear(&mut self) {
225 self.evaluations.clear();
226 self.metadata.total_entries = 0;
227 self.metadata.cache_size_bytes = 0;
228 self.metadata.last_cleanup = SystemTime::now();
229 }
230
231 pub fn contains(&self, key: &str) -> bool {
233 self.evaluations.contains_key(key)
234 }
235
236 pub fn len(&self) -> usize {
238 self.evaluations.len()
239 }
240
241 pub fn is_empty(&self) -> bool {
243 self.evaluations.is_empty()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn results(score: f64) -> EvaluationResults<f64> {
252 EvaluationResults {
253 metric_scores: std::collections::HashMap::new(),
254 overall_score: score,
255 confidence_intervals: std::collections::HashMap::new(),
256 evaluation_time: std::time::Duration::from_secs(100),
257 success: true,
258 error_message: None,
259 cv_results: None,
260 benchmark_results: std::collections::HashMap::new(),
261 training_trajectory: vec![],
262 }
263 }
264
265 #[test]
266 fn test_evaluation_cache() {
267 let mut cache = EvaluationCache::<f64>::new();
268 assert_eq!(cache.metadata.total_entries, 0);
269
270 cache.insert("test_key".to_string(), results(0.95));
271 assert_eq!(cache.metadata.total_entries, 1);
272 assert!(cache.contains("test_key"));
273 }
274
275 #[test]
276 fn test_access_count_is_incremented_on_hits() {
277 let mut cache = EvaluationCache::<f64>::new();
278 cache.insert("k".to_string(), results(0.5));
279
280 assert_eq!(cache.total_accesses(), 0);
282
283 for expected in 1..=3 {
284 let hit = cache.get("k").expect("hit");
285 assert_eq!(hit.access_count, expected);
286 }
287 assert_eq!(cache.total_accesses(), 3);
288 assert!(cache.get("missing").is_none());
289 }
290
291 #[test]
292 fn test_lru_eviction_bounds_the_cache() {
293 let mut cache = EvaluationCache::<f64>::with_capacity(2, CacheEvictionPolicy::LRU);
294 cache.insert("a".to_string(), results(0.1));
295 cache.insert("b".to_string(), results(0.2));
296
297 assert!(cache.get("a").is_some());
299
300 cache.insert("c".to_string(), results(0.3));
301
302 assert_eq!(cache.len(), 2, "cache must stay within capacity");
303 assert!(cache.contains("a"));
304 assert!(cache.contains("c"));
305 assert!(!cache.contains("b"), "LRU victim must be evicted");
306 }
307
308 #[test]
309 fn test_lfu_eviction_keeps_hot_entries() {
310 let mut cache = EvaluationCache::<f64>::with_capacity(2, CacheEvictionPolicy::LFU);
311 cache.insert("hot".to_string(), results(0.1));
312 cache.insert("cold".to_string(), results(0.2));
313
314 for _ in 0..5 {
315 assert!(cache.get("hot").is_some());
316 }
317
318 cache.insert("new".to_string(), results(0.3));
319
320 assert_eq!(cache.len(), 2);
321 assert!(cache.contains("hot"));
322 assert!(!cache.contains("cold"));
323 }
324
325 #[test]
326 fn test_access_frequency_and_hottest_key_report_real_reuse() {
327 let mut cache = EvaluationCache::<f64>::with_capacity(4, CacheEvictionPolicy::LRU);
328 assert!(cache.hottest_key().is_none());
329
330 cache.insert("cold".to_string(), results(0.1));
331 cache.insert("warm".to_string(), results(0.2));
332 cache.insert("hot".to_string(), results(0.3));
333
334 assert_eq!(cache.access_frequency("hot"), Some(0));
335 assert_eq!(cache.access_frequency("absent"), None);
336
337 for _ in 0..5 {
338 assert!(cache.get("hot").is_some());
339 }
340 assert!(cache.get("warm").is_some());
341
342 assert_eq!(cache.access_frequency("hot"), Some(5));
343 assert_eq!(cache.access_frequency("warm"), Some(1));
344 assert_eq!(cache.access_frequency("cold"), Some(0));
345 assert_eq!(cache.hottest_key(), Some("hot"));
346
347 cache.clear();
349 assert_eq!(cache.access_frequency("hot"), None);
350 assert!(cache.hottest_key().is_none());
351 }
352
353 #[test]
354 fn test_cache_never_exceeds_capacity_under_load() {
355 let mut cache = EvaluationCache::<f64>::with_capacity(8, CacheEvictionPolicy::LRU);
356 for i in 0..1000 {
357 cache.insert(format!("key_{}", i), results(i as f64));
358 assert!(cache.len() <= 8);
359 }
360 assert_eq!(cache.len(), 8);
361 assert_eq!(cache.stats().total_entries, 8);
362 }
363}