Skip to main content

linkrs_query/cache/
stats.rs

1//! Unified Cache Statistics Module
2//!
3//! Provides centralized statistics collection and reporting for all cache types.
4//! Uses the `metrics` crate for production metrics export.
5
6pub use linkrs_metrics::utils::CacheStats;
7
8use parking_lot::RwLock;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::sync::Arc;
11
12/// Memory usage tracking
13#[derive(Debug, Default)]
14pub struct MemoryStats {
15    current_bytes: AtomicUsize,
16    max_bytes: AtomicUsize,
17    entry_count: AtomicUsize,
18}
19
20impl MemoryStats {
21    pub fn new(max_bytes: usize) -> Self {
22        Self {
23            current_bytes: AtomicUsize::new(0),
24            max_bytes: AtomicUsize::new(max_bytes),
25            entry_count: AtomicUsize::new(0),
26        }
27    }
28
29    pub fn update(&self, current_bytes: usize, entry_count: usize) {
30        self.current_bytes.store(current_bytes, Ordering::Relaxed);
31        self.entry_count.store(entry_count, Ordering::Relaxed);
32    }
33
34    pub fn set_max_bytes(&self, max_bytes: usize) {
35        self.max_bytes.store(max_bytes, Ordering::Relaxed);
36    }
37
38    pub fn current_bytes(&self) -> usize {
39        self.current_bytes.load(Ordering::Relaxed)
40    }
41
42    pub fn max_bytes(&self) -> usize {
43        self.max_bytes.load(Ordering::Relaxed)
44    }
45
46    pub fn entry_count(&self) -> usize {
47        self.entry_count.load(Ordering::Relaxed)
48    }
49
50    pub fn usage_ratio(&self) -> f64 {
51        let max = self.max_bytes();
52        if max == 0 {
53            0.0
54        } else {
55            self.current_bytes() as f64 / max as f64
56        }
57    }
58
59    pub fn reset(&self) {
60        self.current_bytes.store(0, Ordering::Relaxed);
61        self.entry_count.store(0, Ordering::Relaxed);
62    }
63}
64
65impl Clone for MemoryStats {
66    fn clone(&self) -> Self {
67        Self {
68            current_bytes: AtomicUsize::new(self.current_bytes()),
69            max_bytes: AtomicUsize::new(self.max_bytes()),
70            entry_count: AtomicUsize::new(self.entry_count()),
71        }
72    }
73}
74
75/// Plan cache specific statistics
76#[derive(Debug)]
77pub struct PlanCacheStats {
78    pub counters: CacheStats,
79    pub memory: MemoryStats,
80    pub avg_query_size: Arc<RwLock<usize>>,
81    pub total_query_size: AtomicUsize,
82}
83
84impl Clone for PlanCacheStats {
85    fn clone(&self) -> Self {
86        Self {
87            counters: self.counters.clone(),
88            memory: self.memory.clone(),
89            avg_query_size: Arc::new(RwLock::new(*self.avg_query_size.read())),
90            total_query_size: AtomicUsize::new(self.total_query_size.load(Ordering::Relaxed)),
91        }
92    }
93}
94
95impl PlanCacheStats {
96    pub fn new(memory_budget: usize) -> Self {
97        Self {
98            counters: CacheStats::new(),
99            memory: MemoryStats::new(memory_budget),
100            avg_query_size: Arc::new(RwLock::new(0)),
101            total_query_size: AtomicUsize::new(0),
102        }
103    }
104
105    pub fn record_query_size(&self, size: usize) {
106        self.total_query_size.fetch_add(size, Ordering::Relaxed);
107        let total = self.total_query_size.load(Ordering::Relaxed);
108        let count = self.memory.entry_count();
109        if let Some(avg) = total.checked_div(count) {
110            *self.avg_query_size.write() = avg;
111        }
112    }
113
114    pub fn avg_query_size(&self) -> usize {
115        *self.avg_query_size.read()
116    }
117
118    pub fn estimated_memory(&self) -> usize {
119        const PER_ENTRY_OVERHEAD: usize = 1024;
120        let total_query = self.total_query_size.load(Ordering::Relaxed);
121        let entries = self.memory.entry_count();
122        total_query + (entries * PER_ENTRY_OVERHEAD)
123    }
124
125    pub fn hit_rate(&self) -> f64 {
126        self.counters.hit_rate()
127    }
128
129    pub fn reset(&self) {
130        self.counters.reset();
131        self.memory.reset();
132        self.total_query_size.store(0, Ordering::Relaxed);
133        *self.avg_query_size.write() = 0;
134    }
135
136    pub fn snapshot(&self) -> PlanCacheStatsSnapshot {
137        PlanCacheStatsSnapshot {
138            hits: self.counters.hits(),
139            misses: self.counters.misses(),
140            evictions: self.counters.evictions(),
141            expirations: self.counters.expirations(),
142            entry_count: self.memory.entry_count(),
143            current_memory: self.memory.current_bytes(),
144            max_memory: self.memory.max_bytes(),
145            hit_rate: self.hit_rate(),
146            avg_query_size: self.avg_query_size(),
147        }
148    }
149}
150
151impl Default for PlanCacheStats {
152    fn default() -> Self {
153        Self::new(50 * 1024 * 1024)
154    }
155}
156
157/// Plan cache statistics snapshot (for reporting)
158#[derive(Debug, Clone, Default)]
159pub struct PlanCacheStatsSnapshot {
160    pub hits: u64,
161    pub misses: u64,
162    pub evictions: u64,
163    pub expirations: u64,
164    pub entry_count: usize,
165    pub current_memory: usize,
166    pub max_memory: usize,
167    pub hit_rate: f64,
168    pub avg_query_size: usize,
169}
170
171/// CTE cache specific statistics
172#[derive(Debug, Clone)]
173pub struct CteCacheStats {
174    pub counters: CacheStats,
175    pub memory: MemoryStats,
176}
177
178impl CteCacheStats {
179    pub fn new(max_size: usize) -> Self {
180        Self {
181            counters: CacheStats::new(),
182            memory: MemoryStats::new(max_size),
183        }
184    }
185
186    pub fn hit_rate(&self) -> f64 {
187        self.counters.hit_rate()
188    }
189
190    pub fn memory_usage_ratio(&self) -> f64 {
191        self.memory.usage_ratio()
192    }
193
194    pub fn reset(&self) {
195        self.counters.reset();
196        self.memory.reset();
197    }
198
199    pub fn snapshot(&self) -> CteCacheStatsSnapshot {
200        CteCacheStatsSnapshot {
201            hits: self.counters.hits(),
202            misses: self.counters.misses(),
203            evictions: self.counters.evictions(),
204            rejections: self.counters.rejections(),
205            entry_count: self.memory.entry_count(),
206            current_memory: self.memory.current_bytes(),
207            max_memory: self.memory.max_bytes(),
208            hit_rate: self.hit_rate(),
209            memory_usage_ratio: self.memory_usage_ratio(),
210        }
211    }
212}
213
214impl Default for CteCacheStats {
215    fn default() -> Self {
216        Self::new(64 * 1024 * 1024)
217    }
218}
219
220/// CTE cache statistics snapshot (for reporting)
221#[derive(Debug, Clone, Default)]
222pub struct CteCacheStatsSnapshot {
223    pub hits: u64,
224    pub misses: u64,
225    pub evictions: u64,
226    pub rejections: u64,
227    pub entry_count: usize,
228    pub current_memory: usize,
229    pub max_memory: usize,
230    pub hit_rate: f64,
231    pub memory_usage_ratio: f64,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_cache_stats() {
240        let stats = CacheStats::new();
241
242        stats.record_hit();
243        stats.record_hit();
244        stats.record_miss();
245
246        assert_eq!(stats.hits(), 2);
247        assert_eq!(stats.misses(), 1);
248        assert_eq!(stats.total_requests(), 3);
249        assert!((stats.hit_rate() - 0.6666666666666666).abs() < 0.01);
250    }
251
252    #[test]
253    fn test_cache_stats_reset() {
254        let stats = CacheStats::new();
255        stats.record_hit();
256        stats.record_miss();
257        stats.reset();
258
259        assert_eq!(stats.hits(), 0);
260        assert_eq!(stats.misses(), 0);
261    }
262
263    #[test]
264    fn test_memory_stats() {
265        let stats = MemoryStats::new(1000);
266        stats.update(500, 10);
267
268        assert_eq!(stats.current_bytes(), 500);
269        assert_eq!(stats.entry_count(), 10);
270        assert_eq!(stats.usage_ratio(), 0.5);
271    }
272
273    #[test]
274    fn test_plan_cache_stats() {
275        let stats = PlanCacheStats::new(1024 * 1024);
276
277        stats.counters.record_hit();
278        stats.counters.record_miss();
279        stats.memory.update(512 * 1024, 5);
280
281        let snapshot = stats.snapshot();
282        assert_eq!(snapshot.hits, 1);
283        assert_eq!(snapshot.misses, 1);
284        assert_eq!(snapshot.entry_count, 5);
285    }
286
287    #[test]
288    fn test_cte_cache_stats() {
289        let stats = CteCacheStats::new(1024 * 1024);
290
291        stats.counters.record_hit();
292        stats.counters.record_hit();
293        stats.memory.update(256 * 1024, 3);
294
295        let snapshot = stats.snapshot();
296        assert_eq!(snapshot.hits, 2);
297        assert_eq!(snapshot.entry_count, 3);
298        assert!((snapshot.hit_rate - 1.0).abs() < 0.01);
299    }
300}