aether/sandbox/
module_cache.rs1use crate::value::Value;
6use std::collections::HashMap;
7use std::sync::RwLock;
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone)]
12struct ModuleCacheEntry {
13 exports: HashMap<String, Value>,
15 loaded_at: Instant,
17 #[allow(dead_code)]
19 access_count: usize,
20}
21
22#[derive(Debug, Clone)]
24pub struct ModuleCacheStats {
25 pub module_count: usize,
27 pub total_loads: usize,
29 pub cache_hits: usize,
31 pub cache_misses: usize,
33 pub hit_rate: f64,
35}
36
37pub struct ModuleCacheManager {
39 cache: RwLock<HashMap<String, ModuleCacheEntry>>,
41 max_size: usize,
43 ttl_secs: u64,
45 stats: RwLock<ModuleCacheStats>,
47}
48
49impl ModuleCacheManager {
50 pub fn new(max_size: usize, ttl_secs: u64) -> Self {
52 Self {
53 cache: RwLock::new(HashMap::new()),
54 max_size,
55 ttl_secs,
56 stats: RwLock::new(ModuleCacheStats {
57 module_count: 0,
58 total_loads: 0,
59 cache_hits: 0,
60 cache_misses: 0,
61 hit_rate: 0.0,
62 }),
63 }
64 }
65
66 pub fn get(&self, module_id: &str) -> Option<HashMap<String, Value>> {
68 let cache = self.cache.read().ok()?;
69 let entry = cache.get(module_id)?;
70
71 if self.ttl_secs > 0 {
73 let elapsed = entry.loaded_at.elapsed().as_secs();
74 if elapsed > self.ttl_secs {
75 return None; }
77 }
78
79 if let Ok(mut stats) = self.stats.write() {
81 stats.cache_hits += 1;
82 stats.total_loads += 1;
83 if stats.total_loads > 0 {
84 stats.hit_rate = stats.cache_hits as f64 / stats.total_loads as f64;
85 }
86 }
87
88 Some(entry.exports.clone())
89 }
90
91 pub fn insert(&self, module_id: String, exports: HashMap<String, Value>) {
93 if self.max_size > 0 {
95 let mut cache = self.cache.write().unwrap();
96 if cache.len() >= self.max_size {
97 let to_remove = (self.max_size / 10).max(1);
99 self.evict_oldest(&mut cache, to_remove);
100 }
101
102 cache.insert(
103 module_id.clone(),
104 ModuleCacheEntry {
105 exports,
106 loaded_at: Instant::now(),
107 access_count: 0,
108 },
109 );
110
111 if let Ok(mut stats) = self.stats.write() {
113 stats.cache_misses += 1;
114 stats.total_loads += 1;
115 stats.module_count = cache.len();
116 if stats.total_loads > 0 {
117 stats.hit_rate = stats.cache_hits as f64 / stats.total_loads as f64;
118 }
119 }
120 }
121 }
122
123 pub fn cleanup_expired(&self) {
125 if self.ttl_secs == 0 {
126 return;
127 }
128
129 let ttl = Duration::from_secs(self.ttl_secs);
130 let mut cache = self.cache.write().unwrap();
131 let now = Instant::now();
132
133 cache.retain(|_, entry| now.duration_since(entry.loaded_at) < ttl);
134
135 if let Ok(mut stats) = self.stats.write() {
136 stats.module_count = cache.len();
137 }
138 }
139
140 pub fn clear(&self) {
142 let mut cache = self.cache.write().unwrap();
143 cache.clear();
144
145 if let Ok(mut stats) = self.stats.write() {
146 stats.module_count = 0;
147 }
149 }
150
151 pub fn remove(&self, module_id: &str) -> bool {
153 let mut cache = self.cache.write().unwrap();
154 let removed = cache.remove(module_id).is_some();
155
156 if removed && let Ok(mut stats) = self.stats.write() {
157 stats.module_count = cache.len();
158 }
159
160 removed
161 }
162
163 pub fn stats(&self) -> ModuleCacheStats {
165 self.stats.read().unwrap().clone()
166 }
167
168 pub fn cached_modules(&self) -> Vec<String> {
170 self.cache.read().unwrap().keys().cloned().collect()
171 }
172
173 fn evict_oldest(&self, cache: &mut HashMap<String, ModuleCacheEntry>, count: usize) {
175 let mut entries: Vec<_> = cache.iter().collect();
177 entries.sort_by_key(|(_, entry)| entry.loaded_at);
178
179 let to_remove: Vec<String> = entries
180 .iter()
181 .take(count)
182 .map(|(module_id, _)| (*module_id).clone())
183 .collect();
184
185 for module_id in to_remove {
187 cache.remove(&module_id);
188 }
189 }
190}
191
192impl Default for ModuleCacheManager {
193 fn default() -> Self {
194 Self::new(100, 0)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_module_cache_basic() {
204 let manager = ModuleCacheManager::new(10, 0);
205
206 let mut exports = HashMap::new();
208 exports.insert("foo".to_string(), Value::Number(42.0));
209 manager.insert("test_module".to_string(), exports.clone());
210
211 let retrieved = manager.get("test_module");
213 assert!(retrieved.is_some());
214 assert_eq!(retrieved.unwrap().get("foo").unwrap(), &Value::Number(42.0));
215 }
216
217 #[test]
218 fn test_module_cache_miss() {
219 let manager = ModuleCacheManager::new(10, 0);
220
221 let retrieved = manager.get("nonexistent");
223 assert!(retrieved.is_none());
224
225 let stats = manager.stats();
227 assert_eq!(stats.cache_misses, 0); assert_eq!(stats.cache_hits, 0);
229 }
230
231 #[test]
232 fn test_module_cache_max_size() {
233 let manager = ModuleCacheManager::new(3, 0); let exports = HashMap::new();
235
236 for i in 0..5 {
238 manager.insert(format!("module{}", i), exports.clone());
239 }
240
241 assert_eq!(manager.cached_modules().len(), 3);
243 assert_eq!(manager.stats().module_count, 3);
244 }
245
246 #[test]
247 fn test_module_cache_clear() {
248 let manager = ModuleCacheManager::new(10, 0);
249 let mut exports = HashMap::new();
250 exports.insert("foo".to_string(), Value::Number(42.0));
251
252 manager.insert("test".to_string(), exports);
253 assert_eq!(manager.cached_modules().len(), 1);
254
255 manager.clear();
256 assert_eq!(manager.cached_modules().len(), 0);
257 }
258
259 #[test]
260 fn test_module_cache_remove() {
261 let manager = ModuleCacheManager::new(10, 0);
262 let mut exports = HashMap::new();
263 exports.insert("foo".to_string(), Value::Number(42.0));
264
265 manager.insert("test".to_string(), exports);
266 assert!(manager.remove("test"));
267 assert!(!manager.remove("nonexistent"));
268 assert_eq!(manager.cached_modules().len(), 0);
269 }
270
271 #[test]
272 fn test_module_cache_ttl() {
273 let manager = ModuleCacheManager::new(10, 1); let mut exports = HashMap::new();
275 exports.insert("foo".to_string(), Value::Number(42.0));
276
277 manager.insert("test".to_string(), exports.clone());
278
279 assert!(manager.get("test").is_some());
281
282 std::thread::sleep(Duration::from_secs(2));
284 assert!(manager.get("test").is_none());
285 }
286
287 #[test]
288 fn test_module_cache_stats() {
289 let manager = ModuleCacheManager::new(10, 0);
290 let mut exports = HashMap::new();
291 exports.insert("foo".to_string(), Value::Number(42.0));
292
293 manager.insert("test".to_string(), exports.clone());
295
296 manager.get("test");
298 manager.get("test");
299
300 manager.get("nonexistent");
302
303 let stats = manager.stats();
304 assert_eq!(stats.total_loads, 3);
305 assert_eq!(stats.cache_hits, 2);
306 assert_eq!(stats.cache_misses, 1);
307 assert!((stats.hit_rate - 0.666).abs() < 0.01); }
309}