ricecoder_research/
cache_stats.rs

1//! Cache statistics and performance metrics
2
3use serde::{Deserialize, Serialize};
4use std::sync::{Arc, RwLock};
5use std::time::{Duration, SystemTime};
6
7/// Detailed cache statistics with performance metrics
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DetailedCacheStats {
10    /// Total number of cache hits
11    pub hits: u64,
12    /// Total number of cache misses
13    pub misses: u64,
14    /// Total number of cache invalidations
15    pub invalidations: u64,
16    /// Current cache size in bytes
17    pub size_bytes: u64,
18    /// Number of entries in cache
19    pub entry_count: usize,
20    /// Average time to retrieve from cache (milliseconds)
21    pub avg_retrieval_time_ms: f64,
22    /// Average time to store in cache (milliseconds)
23    pub avg_store_time_ms: f64,
24    /// Total time spent on cache operations (milliseconds)
25    pub total_operation_time_ms: f64,
26    /// Timestamp of last cache operation
27    pub last_operation_time: Option<SystemTime>,
28    /// Timestamp of cache creation
29    pub created_at: SystemTime,
30}
31
32impl DetailedCacheStats {
33    /// Calculate hit rate as a percentage (0.0 to 100.0)
34    pub fn hit_rate(&self) -> f64 {
35        let total = self.hits + self.misses;
36        if total == 0 {
37            0.0
38        } else {
39            (self.hits as f64 / total as f64) * 100.0
40        }
41    }
42
43    /// Calculate miss rate as a percentage (0.0 to 100.0)
44    pub fn miss_rate(&self) -> f64 {
45        100.0 - self.hit_rate()
46    }
47
48    /// Calculate invalidation rate (invalidations per total operations)
49    pub fn invalidation_rate(&self) -> f64 {
50        let total = self.hits + self.misses;
51        if total == 0 {
52            0.0
53        } else {
54            (self.invalidations as f64 / total as f64) * 100.0
55        }
56    }
57
58    /// Get cache efficiency score (0.0 to 100.0)
59    /// Higher is better: considers hit rate and invalidation rate
60    pub fn efficiency_score(&self) -> f64 {
61        let hit_rate = self.hit_rate();
62        let invalidation_rate = self.invalidation_rate();
63        // Efficiency = hit_rate - (invalidation_rate * 0.5)
64        // This penalizes frequent invalidations but not as heavily as misses
65        (hit_rate - (invalidation_rate * 0.5)).max(0.0)
66    }
67
68    /// Get uptime since cache creation
69    pub fn uptime(&self) -> Duration {
70        SystemTime::now()
71            .duration_since(self.created_at)
72            .unwrap_or(Duration::from_secs(0))
73    }
74}
75
76/// Tracks cache operation timings
77#[derive(Debug, Clone)]
78pub struct CacheOperationTimer {
79    /// Start time of the operation
80    start_time: SystemTime,
81}
82
83impl CacheOperationTimer {
84    /// Create a new timer
85    pub fn start() -> Self {
86        Self {
87            start_time: SystemTime::now(),
88        }
89    }
90
91    /// Get elapsed time in milliseconds
92    pub fn elapsed_ms(&self) -> f64 {
93        self.start_time
94            .elapsed()
95            .unwrap_or(Duration::from_secs(0))
96            .as_secs_f64()
97            * 1000.0
98    }
99}
100
101/// Thread-safe cache statistics tracker
102#[derive(Debug, Clone)]
103pub struct CacheStatsTracker {
104    stats: Arc<RwLock<DetailedCacheStats>>,
105}
106
107impl CacheStatsTracker {
108    /// Create a new statistics tracker
109    pub fn new() -> Self {
110        Self {
111            stats: Arc::new(RwLock::new(DetailedCacheStats {
112                hits: 0,
113                misses: 0,
114                invalidations: 0,
115                size_bytes: 0,
116                entry_count: 0,
117                avg_retrieval_time_ms: 0.0,
118                avg_store_time_ms: 0.0,
119                total_operation_time_ms: 0.0,
120                last_operation_time: None,
121                created_at: SystemTime::now(),
122            })),
123        }
124    }
125
126    /// Record a cache hit with timing
127    pub fn record_hit(&self, retrieval_time_ms: f64) {
128        if let Ok(mut stats) = self.stats.write() {
129            stats.hits += 1;
130            stats.total_operation_time_ms += retrieval_time_ms;
131
132            // Update average retrieval time
133            let total_retrievals = stats.hits + stats.misses;
134            if total_retrievals > 0 {
135                stats.avg_retrieval_time_ms =
136                    stats.total_operation_time_ms / total_retrievals as f64;
137            }
138
139            stats.last_operation_time = Some(SystemTime::now());
140        }
141    }
142
143    /// Record a cache miss
144    pub fn record_miss(&self) {
145        if let Ok(mut stats) = self.stats.write() {
146            stats.misses += 1;
147            stats.last_operation_time = Some(SystemTime::now());
148        }
149    }
150
151    /// Record a cache store operation with timing
152    pub fn record_store(&self, store_time_ms: f64, size_bytes: u64) {
153        if let Ok(mut stats) = self.stats.write() {
154            stats.total_operation_time_ms += store_time_ms;
155            stats.size_bytes = size_bytes;
156
157            // Update average store time
158            let total_operations = stats.hits + stats.misses;
159            if total_operations > 0 {
160                stats.avg_store_time_ms = store_time_ms / total_operations as f64;
161            }
162
163            stats.last_operation_time = Some(SystemTime::now());
164        }
165    }
166
167    /// Record a cache invalidation
168    pub fn record_invalidation(&self) {
169        if let Ok(mut stats) = self.stats.write() {
170            stats.invalidations += 1;
171            stats.last_operation_time = Some(SystemTime::now());
172        }
173    }
174
175    /// Update entry count
176    pub fn set_entry_count(&self, count: usize) {
177        if let Ok(mut stats) = self.stats.write() {
178            stats.entry_count = count;
179        }
180    }
181
182    /// Get current statistics
183    pub fn get_stats(&self) -> Option<DetailedCacheStats> {
184        self.stats.read().ok().map(|s| s.clone())
185    }
186
187    /// Reset all statistics
188    pub fn reset(&self) {
189        if let Ok(mut stats) = self.stats.write() {
190            stats.hits = 0;
191            stats.misses = 0;
192            stats.invalidations = 0;
193            stats.size_bytes = 0;
194            stats.entry_count = 0;
195            stats.avg_retrieval_time_ms = 0.0;
196            stats.avg_store_time_ms = 0.0;
197            stats.total_operation_time_ms = 0.0;
198            stats.last_operation_time = None;
199            stats.created_at = SystemTime::now();
200        }
201    }
202
203    /// Get a formatted summary of cache statistics
204    pub fn summary(&self) -> String {
205        if let Some(stats) = self.get_stats() {
206            format!(
207                "Cache Statistics:\n  Hits: {}\n  Misses: {}\n  Hit Rate: {:.2}%\n  Invalidations: {}\n  Entries: {}\n  Size: {} bytes\n  Avg Retrieval: {:.2}ms\n  Avg Store: {:.2}ms\n  Efficiency Score: {:.2}",
208                stats.hits,
209                stats.misses,
210                stats.hit_rate(),
211                stats.invalidations,
212                stats.entry_count,
213                stats.size_bytes,
214                stats.avg_retrieval_time_ms,
215                stats.avg_store_time_ms,
216                stats.efficiency_score()
217            )
218        } else {
219            "Cache Statistics: (unavailable)".to_string()
220        }
221    }
222}
223
224impl Default for CacheStatsTracker {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_detailed_cache_stats_hit_rate() {
236        let stats = DetailedCacheStats {
237            hits: 75,
238            misses: 25,
239            invalidations: 0,
240            size_bytes: 1024,
241            entry_count: 10,
242            avg_retrieval_time_ms: 1.5,
243            avg_store_time_ms: 2.0,
244            total_operation_time_ms: 100.0,
245            last_operation_time: None,
246            created_at: SystemTime::now(),
247        };
248
249        assert_eq!(stats.hit_rate(), 75.0);
250        assert_eq!(stats.miss_rate(), 25.0);
251    }
252
253    #[test]
254    fn test_detailed_cache_stats_invalidation_rate() {
255        let stats = DetailedCacheStats {
256            hits: 80,
257            misses: 20,
258            invalidations: 10,
259            size_bytes: 1024,
260            entry_count: 10,
261            avg_retrieval_time_ms: 1.5,
262            avg_store_time_ms: 2.0,
263            total_operation_time_ms: 100.0,
264            last_operation_time: None,
265            created_at: SystemTime::now(),
266        };
267
268        assert_eq!(stats.invalidation_rate(), 10.0);
269    }
270
271    #[test]
272    fn test_detailed_cache_stats_efficiency_score() {
273        let stats = DetailedCacheStats {
274            hits: 90,
275            misses: 10,
276            invalidations: 5,
277            size_bytes: 1024,
278            entry_count: 10,
279            avg_retrieval_time_ms: 1.5,
280            avg_store_time_ms: 2.0,
281            total_operation_time_ms: 100.0,
282            last_operation_time: None,
283            created_at: SystemTime::now(),
284        };
285
286        let efficiency = stats.efficiency_score();
287        // hit_rate = 90%, invalidation_rate = 5%
288        // efficiency = 90 - (5 * 0.5) = 87.5
289        assert!((efficiency - 87.5).abs() < 0.01);
290    }
291
292    #[test]
293    fn test_cache_operation_timer() {
294        let timer = CacheOperationTimer::start();
295        std::thread::sleep(Duration::from_millis(10));
296        let elapsed = timer.elapsed_ms();
297        assert!(elapsed >= 10.0);
298    }
299
300    #[test]
301    fn test_cache_stats_tracker_record_hit() {
302        let tracker = CacheStatsTracker::new();
303        tracker.record_hit(1.5);
304
305        let stats = tracker.get_stats().unwrap();
306        assert_eq!(stats.hits, 1);
307        assert_eq!(stats.misses, 0);
308    }
309
310    #[test]
311    fn test_cache_stats_tracker_record_miss() {
312        let tracker = CacheStatsTracker::new();
313        tracker.record_miss();
314
315        let stats = tracker.get_stats().unwrap();
316        assert_eq!(stats.hits, 0);
317        assert_eq!(stats.misses, 1);
318    }
319
320    #[test]
321    fn test_cache_stats_tracker_record_store() {
322        let tracker = CacheStatsTracker::new();
323        tracker.record_store(2.0, 1024);
324
325        let stats = tracker.get_stats().unwrap();
326        assert_eq!(stats.size_bytes, 1024);
327    }
328
329    #[test]
330    fn test_cache_stats_tracker_record_invalidation() {
331        let tracker = CacheStatsTracker::new();
332        tracker.record_invalidation();
333
334        let stats = tracker.get_stats().unwrap();
335        assert_eq!(stats.invalidations, 1);
336    }
337
338    #[test]
339    fn test_cache_stats_tracker_reset() {
340        let tracker = CacheStatsTracker::new();
341        tracker.record_hit(1.5);
342        tracker.record_miss();
343        tracker.record_invalidation();
344
345        let stats_before = tracker.get_stats().unwrap();
346        assert!(stats_before.hits > 0 || stats_before.misses > 0);
347
348        tracker.reset();
349
350        let stats_after = tracker.get_stats().unwrap();
351        assert_eq!(stats_after.hits, 0);
352        assert_eq!(stats_after.misses, 0);
353        assert_eq!(stats_after.invalidations, 0);
354    }
355
356    #[test]
357    fn test_cache_stats_tracker_summary() {
358        let tracker = CacheStatsTracker::new();
359        tracker.record_hit(1.5);
360        tracker.record_miss();
361
362        let summary = tracker.summary();
363        assert!(summary.contains("Cache Statistics"));
364        assert!(summary.contains("Hits: 1"));
365        assert!(summary.contains("Misses: 1"));
366    }
367
368    #[test]
369    fn test_cache_stats_tracker_set_entry_count() {
370        let tracker = CacheStatsTracker::new();
371        tracker.set_entry_count(42);
372
373        let stats = tracker.get_stats().unwrap();
374        assert_eq!(stats.entry_count, 42);
375    }
376}