Skip to main content

agentic_evolve_core/cache/
metrics.rs

1//! Cache-level metrics tracking via atomic counters.
2
3use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
4
5use serde::{Deserialize, Serialize};
6
7/// Thread-safe cache performance metrics.
8pub struct CacheMetrics {
9    hit_count: AtomicU64,
10    miss_count: AtomicU64,
11    eviction_count: AtomicU64,
12    current_size: AtomicUsize,
13}
14
15/// Serializable snapshot of cache metrics.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CacheMetricsSnapshot {
18    pub hit_count: u64,
19    pub miss_count: u64,
20    pub eviction_count: u64,
21    pub current_size: usize,
22    pub hit_rate: f64,
23}
24
25impl CacheMetrics {
26    /// Create zeroed metrics.
27    pub fn new() -> Self {
28        Self {
29            hit_count: AtomicU64::new(0),
30            miss_count: AtomicU64::new(0),
31            eviction_count: AtomicU64::new(0),
32            current_size: AtomicUsize::new(0),
33        }
34    }
35
36    /// Record a cache hit.
37    pub fn record_hit(&self) {
38        self.hit_count.fetch_add(1, Ordering::Relaxed);
39    }
40
41    /// Record a cache miss.
42    pub fn record_miss(&self) {
43        self.miss_count.fetch_add(1, Ordering::Relaxed);
44    }
45
46    /// Record an eviction.
47    pub fn record_eviction(&self) {
48        self.eviction_count.fetch_add(1, Ordering::Relaxed);
49    }
50
51    /// Set the current cache size.
52    pub fn set_size(&self, size: usize) {
53        self.current_size.store(size, Ordering::Relaxed);
54    }
55
56    /// Get the hit count.
57    pub fn hit_count(&self) -> u64 {
58        self.hit_count.load(Ordering::Relaxed)
59    }
60
61    /// Get the miss count.
62    pub fn miss_count(&self) -> u64 {
63        self.miss_count.load(Ordering::Relaxed)
64    }
65
66    /// Get the eviction count.
67    pub fn eviction_count(&self) -> u64 {
68        self.eviction_count.load(Ordering::Relaxed)
69    }
70
71    /// Get the current size.
72    pub fn current_size(&self) -> usize {
73        self.current_size.load(Ordering::Relaxed)
74    }
75
76    /// Compute the hit rate as a fraction in `[0.0, 1.0]`.
77    ///
78    /// Returns `0.0` if no requests have been made.
79    pub fn hit_rate(&self) -> f64 {
80        let hits = self.hit_count() as f64;
81        let total = hits + self.miss_count() as f64;
82        if total == 0.0 {
83            0.0
84        } else {
85            hits / total
86        }
87    }
88
89    /// Take a serializable snapshot of current metrics.
90    pub fn snapshot(&self) -> CacheMetricsSnapshot {
91        CacheMetricsSnapshot {
92            hit_count: self.hit_count(),
93            miss_count: self.miss_count(),
94            eviction_count: self.eviction_count(),
95            current_size: self.current_size(),
96            hit_rate: self.hit_rate(),
97        }
98    }
99}
100
101impl Default for CacheMetrics {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn new_metrics_are_zero() {
113        let m = CacheMetrics::new();
114        assert_eq!(m.hit_count(), 0);
115        assert_eq!(m.miss_count(), 0);
116        assert_eq!(m.eviction_count(), 0);
117        assert_eq!(m.current_size(), 0);
118    }
119
120    #[test]
121    fn hit_rate_empty_is_zero() {
122        let m = CacheMetrics::new();
123        assert_eq!(m.hit_rate(), 0.0);
124    }
125
126    #[test]
127    fn hit_rate_all_hits() {
128        let m = CacheMetrics::new();
129        m.record_hit();
130        m.record_hit();
131        assert_eq!(m.hit_rate(), 1.0);
132    }
133
134    #[test]
135    fn hit_rate_mixed() {
136        let m = CacheMetrics::new();
137        m.record_hit();
138        m.record_miss();
139        assert!((m.hit_rate() - 0.5).abs() < f64::EPSILON);
140    }
141
142    #[test]
143    fn eviction_count_tracks() {
144        let m = CacheMetrics::new();
145        m.record_eviction();
146        m.record_eviction();
147        assert_eq!(m.eviction_count(), 2);
148    }
149
150    #[test]
151    fn set_size_works() {
152        let m = CacheMetrics::new();
153        m.set_size(42);
154        assert_eq!(m.current_size(), 42);
155    }
156
157    #[test]
158    fn snapshot_captures_state() {
159        let m = CacheMetrics::new();
160        m.record_hit();
161        m.record_miss();
162        m.set_size(10);
163        let snap = m.snapshot();
164        assert_eq!(snap.hit_count, 1);
165        assert_eq!(snap.miss_count, 1);
166        assert_eq!(snap.current_size, 10);
167        assert!((snap.hit_rate - 0.5).abs() < f64::EPSILON);
168    }
169}