use scirs2_core::numeric::Float;
use std::collections::HashMap;
use std::fmt::Debug;
use std::time::SystemTime;
use super::types::*;
use crate::nas_engine::results::EvaluationResults;
pub const DEFAULT_CACHE_CAPACITY: usize = 4096;
#[derive(Debug)]
pub struct EvaluationCache<T: Float + Debug + Send + Sync + 'static> {
evaluations: HashMap<String, CachedEvaluation<T>>,
metadata: CacheMetadata,
capacity: usize,
eviction_policy: CacheEvictionPolicy,
access_clock: u64,
}
#[derive(Debug, Clone)]
pub struct CachedEvaluation<T: Float + Debug + Send + Sync + 'static> {
pub results: EvaluationResults<T>,
pub timestamp: SystemTime,
pub access_count: usize,
pub last_access_tick: u64,
pub is_valid: bool,
}
#[derive(Debug, Clone)]
pub struct CacheMetadata {
pub total_entries: usize,
pub cache_size_bytes: usize,
pub last_cleanup: SystemTime,
pub version: String,
}
impl<T: Float + Debug + Default + Send + Sync> EvaluationCache<T> {
pub(crate) fn new() -> Self {
Self::with_capacity(DEFAULT_CACHE_CAPACITY, CacheEvictionPolicy::LRU)
}
pub fn with_capacity(capacity: usize, eviction_policy: CacheEvictionPolicy) -> Self {
Self {
evaluations: HashMap::new(),
metadata: CacheMetadata {
total_entries: 0,
cache_size_bytes: 0,
last_cleanup: SystemTime::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
capacity: capacity.max(1),
eviction_policy,
access_clock: 0,
}
}
pub(crate) fn get(&mut self, key: &str) -> Option<&CachedEvaluation<T>> {
self.access_clock = self.access_clock.wrapping_add(1);
let tick = self.access_clock;
let entry = self.evaluations.get_mut(key)?;
if !entry.is_valid {
return None;
}
entry.access_count += 1;
entry.last_access_tick = tick;
self.evaluations.get(key)
}
pub(crate) fn insert(&mut self, key: String, results: EvaluationResults<T>) {
self.access_clock = self.access_clock.wrapping_add(1);
let cached_eval = CachedEvaluation {
results,
timestamp: SystemTime::now(),
access_count: 0,
last_access_tick: self.access_clock,
is_valid: true,
};
self.evaluations.insert(key, cached_eval);
self.evict_if_needed();
self.metadata.total_entries = self.evaluations.len();
}
fn evict_if_needed(&mut self) {
while self.evaluations.len() > self.capacity {
let victim = match self.eviction_policy {
CacheEvictionPolicy::LRU => self
.evaluations
.iter()
.min_by_key(|(_, v)| v.last_access_tick)
.map(|(k, _)| k.clone()),
CacheEvictionPolicy::LFU => self
.evaluations
.iter()
.min_by_key(|(_, v)| (v.access_count, v.last_access_tick))
.map(|(k, _)| k.clone()),
CacheEvictionPolicy::FIFO => self
.evaluations
.iter()
.min_by_key(|(_, v)| v.timestamp)
.map(|(k, _)| k.clone()),
CacheEvictionPolicy::Random => self.evaluations.keys().min().cloned(),
};
match victim {
Some(key) => {
self.evaluations.remove(&key);
}
None => break,
}
}
self.metadata.last_cleanup = SystemTime::now();
}
pub fn stats(&self) -> &CacheMetadata {
&self.metadata
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn total_accesses(&self) -> usize {
self.evaluations.values().map(|v| v.access_count).sum()
}
pub fn access_frequency(&self, key: &str) -> Option<usize> {
self.evaluations.get(key).map(|entry| entry.access_count)
}
pub fn hottest_key(&self) -> Option<&str> {
self.evaluations
.iter()
.max_by(|(left_key, left), (right_key, right)| {
left.access_count
.cmp(&right.access_count)
.then_with(|| right_key.cmp(left_key))
})
.map(|(key, _)| key.as_str())
}
pub fn clear(&mut self) {
self.evaluations.clear();
self.metadata.total_entries = 0;
self.metadata.cache_size_bytes = 0;
self.metadata.last_cleanup = SystemTime::now();
}
pub fn contains(&self, key: &str) -> bool {
self.evaluations.contains_key(key)
}
pub fn len(&self) -> usize {
self.evaluations.len()
}
pub fn is_empty(&self) -> bool {
self.evaluations.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn results(score: f64) -> EvaluationResults<f64> {
EvaluationResults {
metric_scores: std::collections::HashMap::new(),
overall_score: score,
confidence_intervals: std::collections::HashMap::new(),
evaluation_time: std::time::Duration::from_secs(100),
success: true,
error_message: None,
cv_results: None,
benchmark_results: std::collections::HashMap::new(),
training_trajectory: vec![],
}
}
#[test]
fn test_evaluation_cache() {
let mut cache = EvaluationCache::<f64>::new();
assert_eq!(cache.metadata.total_entries, 0);
cache.insert("test_key".to_string(), results(0.95));
assert_eq!(cache.metadata.total_entries, 1);
assert!(cache.contains("test_key"));
}
#[test]
fn test_access_count_is_incremented_on_hits() {
let mut cache = EvaluationCache::<f64>::new();
cache.insert("k".to_string(), results(0.5));
assert_eq!(cache.total_accesses(), 0);
for expected in 1..=3 {
let hit = cache.get("k").expect("hit");
assert_eq!(hit.access_count, expected);
}
assert_eq!(cache.total_accesses(), 3);
assert!(cache.get("missing").is_none());
}
#[test]
fn test_lru_eviction_bounds_the_cache() {
let mut cache = EvaluationCache::<f64>::with_capacity(2, CacheEvictionPolicy::LRU);
cache.insert("a".to_string(), results(0.1));
cache.insert("b".to_string(), results(0.2));
assert!(cache.get("a").is_some());
cache.insert("c".to_string(), results(0.3));
assert_eq!(cache.len(), 2, "cache must stay within capacity");
assert!(cache.contains("a"));
assert!(cache.contains("c"));
assert!(!cache.contains("b"), "LRU victim must be evicted");
}
#[test]
fn test_lfu_eviction_keeps_hot_entries() {
let mut cache = EvaluationCache::<f64>::with_capacity(2, CacheEvictionPolicy::LFU);
cache.insert("hot".to_string(), results(0.1));
cache.insert("cold".to_string(), results(0.2));
for _ in 0..5 {
assert!(cache.get("hot").is_some());
}
cache.insert("new".to_string(), results(0.3));
assert_eq!(cache.len(), 2);
assert!(cache.contains("hot"));
assert!(!cache.contains("cold"));
}
#[test]
fn test_access_frequency_and_hottest_key_report_real_reuse() {
let mut cache = EvaluationCache::<f64>::with_capacity(4, CacheEvictionPolicy::LRU);
assert!(cache.hottest_key().is_none());
cache.insert("cold".to_string(), results(0.1));
cache.insert("warm".to_string(), results(0.2));
cache.insert("hot".to_string(), results(0.3));
assert_eq!(cache.access_frequency("hot"), Some(0));
assert_eq!(cache.access_frequency("absent"), None);
for _ in 0..5 {
assert!(cache.get("hot").is_some());
}
assert!(cache.get("warm").is_some());
assert_eq!(cache.access_frequency("hot"), Some(5));
assert_eq!(cache.access_frequency("warm"), Some(1));
assert_eq!(cache.access_frequency("cold"), Some(0));
assert_eq!(cache.hottest_key(), Some("hot"));
cache.clear();
assert_eq!(cache.access_frequency("hot"), None);
assert!(cache.hottest_key().is_none());
}
#[test]
fn test_cache_never_exceeds_capacity_under_load() {
let mut cache = EvaluationCache::<f64>::with_capacity(8, CacheEvictionPolicy::LRU);
for i in 0..1000 {
cache.insert(format!("key_{}", i), results(i as f64));
assert!(cache.len() <= 8);
}
assert_eq!(cache.len(), 8);
assert_eq!(cache.stats().total_entries, 8);
}
}