use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
#[derive(Debug, Default)]
pub struct CacheStats {
pub hits: AtomicU64,
pub misses: AtomicU64,
pub evictions: AtomicU64,
pub expirations: AtomicU64,
pub total_size: AtomicUsize,
pub insertions: AtomicU64,
}
impl CacheStats {
pub fn new() -> Self {
Self::default()
}
pub fn hit_rate(&self) -> f64 {
let hits = self.hits.load(Ordering::Relaxed) as f64;
let total = hits + self.misses.load(Ordering::Relaxed) as f64;
if total > 0.0 {
hits / total
} else {
0.0
}
}
pub fn total_operations(&self) -> u64 {
self.hits.load(Ordering::Relaxed) + self.misses.load(Ordering::Relaxed)
}
pub fn size_bytes(&self) -> usize {
self.total_size.load(Ordering::Relaxed)
}
pub fn size_mb(&self) -> f64 {
self.size_bytes() as f64 / 1_048_576.0
}
pub fn record_hit(&self) {
self.hits.fetch_add(1, Ordering::Relaxed);
}
pub fn record_miss(&self) {
self.misses.fetch_add(1, Ordering::Relaxed);
}
pub fn record_eviction(&self) {
self.evictions.fetch_add(1, Ordering::Relaxed);
}
pub fn record_expiration(&self) {
self.expirations.fetch_add(1, Ordering::Relaxed);
}
pub fn record_insertion(&self) {
self.insertions.fetch_add(1, Ordering::Relaxed);
}
pub fn update_size(&self, delta: isize) {
if delta >= 0 {
self.total_size.fetch_add(delta as usize, Ordering::Relaxed);
} else {
self.total_size
.fetch_sub((-delta) as usize, Ordering::Relaxed);
}
}
pub fn reset(&self) {
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
self.evictions.store(0, Ordering::Relaxed);
self.expirations.store(0, Ordering::Relaxed);
self.insertions.store(0, Ordering::Relaxed);
self.total_size.store(0, Ordering::Relaxed);
}
pub fn snapshot(&self) -> CacheStatsSnapshot {
CacheStatsSnapshot {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
expirations: self.expirations.load(Ordering::Relaxed),
insertions: self.insertions.load(Ordering::Relaxed),
total_size_bytes: self.total_size.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheStatsSnapshot {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub expirations: u64,
pub insertions: u64,
pub total_size_bytes: usize,
}
impl CacheStatsSnapshot {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total > 0 {
self.hits as f64 / total as f64
} else {
0.0
}
}
pub fn total_operations(&self) -> u64 {
self.hits + self.misses
}
pub fn size_mb(&self) -> f64 {
self.total_size_bytes as f64 / 1_048_576.0
}
pub fn is_effective(&self) -> bool {
self.hit_rate() > 0.6
}
pub fn eviction_rate(&self) -> f64 {
let total = self.total_operations();
if total > 0 {
self.evictions as f64 / total as f64
} else {
0.0
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct MultiLayerStats {
pub l1_query: CacheStatsSnapshot,
pub l2_embedding: CacheStatsSnapshot,
pub l3_context: CacheStatsSnapshot,
pub parse_tree: CacheStatsSnapshot,
}
impl MultiLayerStats {
pub fn overall_hit_rate(&self) -> f64 {
let total_hits = self.l1_query.hits
+ self.l2_embedding.hits
+ self.l3_context.hits
+ self.parse_tree.hits;
let total_ops = self.l1_query.total_operations()
+ self.l2_embedding.total_operations()
+ self.l3_context.total_operations()
+ self.parse_tree.total_operations();
if total_ops > 0 {
total_hits as f64 / total_ops as f64
} else {
0.0
}
}
pub fn total_size_bytes(&self) -> usize {
self.l1_query.total_size_bytes
+ self.l2_embedding.total_size_bytes
+ self.l3_context.total_size_bytes
+ self.parse_tree.total_size_bytes
}
pub fn total_size_mb(&self) -> f64 {
self.total_size_bytes() as f64 / 1_048_576.0
}
pub fn is_effective(&self) -> bool {
self.overall_hit_rate() > 0.6
}
pub fn is_within_memory_target(&self) -> bool {
self.total_size_mb() < 500.0
}
pub fn total_operations(&self) -> u64 {
self.l1_query.total_operations()
+ self.l2_embedding.total_operations()
+ self.l3_context.total_operations()
+ self.parse_tree.total_operations()
}
pub fn total_evictions(&self) -> u64 {
self.l1_query.evictions
+ self.l2_embedding.evictions
+ self.l3_context.evictions
+ self.parse_tree.evictions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_stats_hit_rate() {
let stats = CacheStats::new();
assert_eq!(stats.hit_rate(), 0.0);
for _ in 0..60 {
stats.record_hit();
}
for _ in 0..40 {
stats.record_miss();
}
assert!((stats.hit_rate() - 0.6).abs() < 0.01);
}
#[test]
fn test_cache_stats_operations() {
let stats = CacheStats::new();
stats.record_hit();
stats.record_hit();
stats.record_miss();
assert_eq!(stats.total_operations(), 3);
assert_eq!(stats.hits.load(Ordering::Relaxed), 2);
assert_eq!(stats.misses.load(Ordering::Relaxed), 1);
}
#[test]
fn test_cache_stats_size() {
let stats = CacheStats::new();
stats.update_size(1024);
assert_eq!(stats.size_bytes(), 1024);
stats.update_size(1024);
assert_eq!(stats.size_bytes(), 2048);
stats.update_size(-1024);
assert_eq!(stats.size_bytes(), 1024);
}
#[test]
fn test_cache_stats_reset() {
let stats = CacheStats::new();
stats.record_hit();
stats.record_miss();
stats.record_eviction();
stats.update_size(1024);
stats.reset();
assert_eq!(stats.hits.load(Ordering::Relaxed), 0);
assert_eq!(stats.misses.load(Ordering::Relaxed), 0);
assert_eq!(stats.evictions.load(Ordering::Relaxed), 0);
assert_eq!(stats.size_bytes(), 0);
}
#[test]
fn test_cache_stats_snapshot() {
let stats = CacheStats::new();
stats.record_hit();
stats.record_hit();
stats.record_miss();
stats.update_size(1024);
let snapshot = stats.snapshot();
assert_eq!(snapshot.hits, 2);
assert_eq!(snapshot.misses, 1);
assert_eq!(snapshot.total_size_bytes, 1024);
assert_eq!(snapshot.hit_rate(), 2.0 / 3.0);
}
#[test]
fn test_snapshot_is_effective() {
let mut snapshot = CacheStatsSnapshot {
hits: 70,
misses: 30,
evictions: 0,
expirations: 0,
insertions: 100,
total_size_bytes: 0,
};
assert!(snapshot.is_effective());
snapshot.hits = 50;
snapshot.misses = 50;
assert!(!snapshot.is_effective()); }
#[test]
fn test_multi_layer_stats() {
let stats = MultiLayerStats {
l1_query: CacheStatsSnapshot {
hits: 60,
misses: 40,
evictions: 5,
expirations: 2,
insertions: 100,
total_size_bytes: 10_000_000, },
l2_embedding: CacheStatsSnapshot {
hits: 80,
misses: 20,
evictions: 3,
expirations: 1,
insertions: 100,
total_size_bytes: 50_000_000, },
l3_context: CacheStatsSnapshot {
hits: 70,
misses: 30,
evictions: 4,
expirations: 3,
insertions: 100,
total_size_bytes: 30_000_000, },
parse_tree: CacheStatsSnapshot {
hits: 90,
misses: 10,
evictions: 2,
expirations: 0,
insertions: 100,
total_size_bytes: 20_000_000, },
};
assert!((stats.overall_hit_rate() - 0.75).abs() < 0.01);
let actual_mb = stats.total_size_mb();
assert!(
(actual_mb - 104.9).abs() < 0.2,
"Expected ~104.9 MiB, got {} MiB (total bytes: {})",
actual_mb,
stats.total_size_bytes()
);
assert!(stats.is_effective());
assert!(stats.is_within_memory_target());
assert_eq!(stats.total_operations(), 400);
assert_eq!(stats.total_evictions(), 14);
}
}