lean_ctx/core/
cache_diagnostics.rs1use std::sync::atomic::{AtomicU64, Ordering};
8
9static CACHE_HITS: AtomicU64 = AtomicU64::new(0);
10static CACHE_MISSES: AtomicU64 = AtomicU64::new(0);
11static CACHE_READ_TOKENS: AtomicU64 = AtomicU64::new(0);
12
13pub fn record_hit(read_tokens: u64) {
15 CACHE_HITS.fetch_add(1, Ordering::Relaxed);
16 CACHE_READ_TOKENS.fetch_add(read_tokens, Ordering::Relaxed);
17}
18
19pub fn record_miss() {
21 CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
22}
23
24pub fn hit_rate() -> Option<f64> {
26 let hits = CACHE_HITS.load(Ordering::Relaxed);
27 let misses = CACHE_MISSES.load(Ordering::Relaxed);
28 let total = hits + misses;
29 if total == 0 {
30 return None;
31 }
32 Some(hits as f64 / total as f64)
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct CacheDiagnostics {
38 pub hits: u64,
39 pub misses: u64,
40 pub hit_rate: Option<f64>,
41 pub cache_read_tokens: u64,
42 pub estimated_savings_usd: f64,
43}
44
45impl CacheDiagnostics {
46 pub fn snapshot() -> Self {
48 let hits = CACHE_HITS.load(Ordering::Relaxed);
49 let misses = CACHE_MISSES.load(Ordering::Relaxed);
50 let read_tokens = CACHE_READ_TOKENS.load(Ordering::Relaxed);
51 let total = hits + misses;
52 let rate = if total > 0 {
53 Some(hits as f64 / total as f64)
54 } else {
55 None
56 };
57
58 let savings_per_token = (3.00 - 0.30) / 1_000_000.0;
60 let estimated_savings_usd = read_tokens as f64 * savings_per_token;
61
62 Self {
63 hits,
64 misses,
65 hit_rate: rate,
66 cache_read_tokens: read_tokens,
67 estimated_savings_usd,
68 }
69 }
70
71 pub fn needs_alert(&self, threshold: f64) -> bool {
73 self.hit_rate.is_some_and(|rate| {
74 let total = self.hits + self.misses;
75 total >= 10 && rate < threshold
76 })
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum CacheAlertSeverity {
83 Warning,
84 Critical,
85}
86
87pub fn check_alert() -> Option<(CacheAlertSeverity, String)> {
89 let diag = CacheDiagnostics::snapshot();
90 if let Some(rate) = diag.hit_rate {
91 let total = diag.hits + diag.misses;
92 if total < 10 {
93 return None;
94 }
95 if rate < 0.50 {
96 return Some((
97 CacheAlertSeverity::Critical,
98 format!(
99 "Provider cache hit rate critically low: {:.0}% ({} hits / {} total)",
100 rate * 100.0,
101 diag.hits,
102 total
103 ),
104 ));
105 }
106 if rate < 0.80 {
107 return Some((
108 CacheAlertSeverity::Warning,
109 format!(
110 "Provider cache hit rate below target: {:.0}% ({} hits / {} total)",
111 rate * 100.0,
112 diag.hits,
113 total
114 ),
115 ));
116 }
117 }
118 None
119}
120
121pub fn reset() {
123 CACHE_HITS.store(0, Ordering::Relaxed);
124 CACHE_MISSES.store(0, Ordering::Relaxed);
125 CACHE_READ_TOKENS.store(0, Ordering::Relaxed);
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn setup() {
133 reset();
134 }
135
136 #[test]
137 fn no_data_returns_none() {
138 setup();
139 assert_eq!(hit_rate(), None);
140 }
141
142 #[test]
143 fn hit_rate_calculation() {
144 setup();
145 for _ in 0..8 {
146 record_hit(1000);
147 }
148 for _ in 0..2 {
149 record_miss();
150 }
151 let rate = hit_rate().unwrap();
152 assert!((rate - 0.8).abs() < 0.01);
153 }
154
155 #[test]
156 fn diagnostics_snapshot() {
157 setup();
158 record_hit(100_000);
159 record_miss();
160 let diag = CacheDiagnostics::snapshot();
161 assert_eq!(diag.hits, 1);
162 assert_eq!(diag.misses, 1);
163 assert_eq!(diag.cache_read_tokens, 100_000);
164 assert!(diag.estimated_savings_usd > 0.0);
165 }
166
167 #[test]
168 fn alert_below_threshold() {
169 setup();
170 for _ in 0..3 {
171 record_hit(1000);
172 }
173 for _ in 0..7 {
174 record_miss();
175 }
176 let diag = CacheDiagnostics::snapshot();
177 assert!(diag.needs_alert(0.80));
178 }
179
180 #[test]
181 fn no_alert_when_above_threshold() {
182 setup();
183 for _ in 0..9 {
184 record_hit(1000);
185 }
186 record_miss();
187 let diag = CacheDiagnostics::snapshot();
188 assert!(!diag.needs_alert(0.80));
189 }
190
191 #[test]
192 fn critical_alert_below_50() {
193 setup();
194 for _ in 0..3 {
195 record_hit(1000);
196 }
197 for _ in 0..7 {
198 record_miss();
199 }
200 let alert = check_alert();
201 assert!(alert.is_some());
202 let (severity, _) = alert.unwrap();
203 assert_eq!(severity, CacheAlertSeverity::Critical);
204 }
205}