driven/fusion/
hot_cache.rs1use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9#[derive(Debug)]
11pub struct TemplateCacheEntry {
12 pub data: Vec<u8>,
14 last_access: Instant,
16 access_count: u64,
18 template_hash: u64,
20 source_hash: u64,
22}
23
24impl TemplateCacheEntry {
25 pub fn new(data: Vec<u8>, template_hash: u64, source_hash: u64) -> Self {
27 Self {
28 data,
29 last_access: Instant::now(),
30 access_count: 1,
31 template_hash,
32 source_hash,
33 }
34 }
35
36 fn touch(&mut self) {
38 self.last_access = Instant::now();
39 self.access_count += 1;
40 }
41
42 pub fn is_stale(&self, current_source_hash: u64) -> bool {
44 self.source_hash != current_source_hash
45 }
46
47 pub fn age(&self) -> Duration {
49 self.last_access.elapsed()
50 }
51}
52
53#[derive(Debug)]
55pub struct HotCache {
56 entries: HashMap<u64, TemplateCacheEntry>,
58 max_size: usize,
60 current_size: usize,
62 hits: AtomicU64,
64 misses: AtomicU64,
66 ttl: Duration,
68}
69
70impl HotCache {
71 pub fn new(max_size: usize) -> Self {
73 Self {
74 entries: HashMap::new(),
75 max_size,
76 current_size: 0,
77 hits: AtomicU64::new(0),
78 misses: AtomicU64::new(0),
79 ttl: Duration::from_secs(3600), }
81 }
82
83 pub fn with_ttl(mut self, ttl: Duration) -> Self {
85 self.ttl = ttl;
86 self
87 }
88
89 pub fn get(&mut self, template_hash: u64) -> Option<&[u8]> {
91 if let Some(entry) = self.entries.get_mut(&template_hash) {
93 if entry.age() < self.ttl {
94 entry.touch();
95 self.hits.fetch_add(1, Ordering::Relaxed);
96 return Some(&entry.data);
97 }
98 }
100
101 self.misses.fetch_add(1, Ordering::Relaxed);
102 None
103 }
104
105 pub fn insert(&mut self, template_hash: u64, source_hash: u64, data: Vec<u8>) {
107 let entry_size = data.len();
108
109 while self.current_size + entry_size > self.max_size && !self.entries.is_empty() {
111 self.evict_one();
112 }
113
114 if entry_size > self.max_size {
116 return;
117 }
118
119 if let Some(old) = self.entries.remove(&template_hash) {
121 self.current_size -= old.data.len();
122 }
123
124 let entry = TemplateCacheEntry::new(data, template_hash, source_hash);
126 self.current_size += entry_size;
127 self.entries.insert(template_hash, entry);
128 }
129
130 pub fn invalidate_stale(&mut self, template_hash: u64, current_source_hash: u64) {
132 if let Some(entry) = self.entries.get(&template_hash) {
133 if entry.is_stale(current_source_hash) {
134 if let Some(removed) = self.entries.remove(&template_hash) {
135 self.current_size -= removed.data.len();
136 }
137 }
138 }
139 }
140
141 pub fn clear(&mut self) {
143 self.entries.clear();
144 self.current_size = 0;
145 }
146
147 pub fn stats(&self) -> CacheStats {
149 let hits = self.hits.load(Ordering::Relaxed);
150 let misses = self.misses.load(Ordering::Relaxed);
151
152 CacheStats {
153 entries: self.entries.len(),
154 size_bytes: self.current_size,
155 max_size_bytes: self.max_size,
156 hits,
157 misses,
158 hit_rate: if hits + misses > 0 {
159 hits as f64 / (hits + misses) as f64
160 } else {
161 0.0
162 },
163 }
164 }
165
166 fn evict_one(&mut self) {
169 let oldest = self
171 .entries
172 .iter()
173 .min_by_key(|(_, e)| e.last_access)
174 .map(|(k, _)| *k);
175
176 if let Some(key) = oldest {
177 if let Some(entry) = self.entries.remove(&key) {
178 self.current_size -= entry.data.len();
179 }
180 }
181 }
182}
183
184impl Default for HotCache {
185 fn default() -> Self {
186 Self::new(64 * 1024 * 1024)
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct CacheStats {
194 pub entries: usize,
196 pub size_bytes: usize,
198 pub max_size_bytes: usize,
200 pub hits: u64,
202 pub misses: u64,
204 pub hit_rate: f64,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_cache_hit_miss() {
214 let mut cache = HotCache::new(1024);
215
216 cache.insert(1, 100, vec![1, 2, 3, 4]);
217
218 assert!(cache.get(1).is_some());
219 assert!(cache.get(2).is_none());
220
221 let stats = cache.stats();
222 assert_eq!(stats.hits, 1);
223 assert_eq!(stats.misses, 1);
224 }
225
226 #[test]
227 fn test_cache_eviction() {
228 let mut cache = HotCache::new(100);
229
230 for i in 0..10 {
232 cache.insert(i, i * 10, vec![0u8; 20]);
233 }
234
235 assert!(cache.entries.len() <= 5);
237 }
238
239 #[test]
240 fn test_cache_invalidation() {
241 let mut cache = HotCache::new(1024);
242
243 cache.insert(1, 100, vec![1, 2, 3]);
244
245 cache.invalidate_stale(1, 100);
247 assert!(cache.entries.contains_key(&1));
248
249 cache.invalidate_stale(1, 200);
251 assert!(!cache.entries.contains_key(&1));
252 }
253}