1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4use tokio::sync::RwLock;
5use serde::{Deserialize, Serialize};
6use tracing::{debug, info, warn};
7
8use crate::lsp::hint::SymbolHint;
9
10#[derive(Debug, Clone)]
12pub struct HintCache {
13 cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
14 max_entries: usize,
15 ttl: Duration,
16}
17
18#[derive(Debug, Clone)]
19struct CacheEntry {
20 hints: Vec<SymbolHint>,
21 created_at: Instant,
22 file_hash: u64,
23 access_count: u64,
24}
25
26impl HintCache {
27 pub fn new(max_entries: usize, ttl_hours: u64) -> Self {
29 Self {
30 cache: Arc::new(RwLock::new(HashMap::new())),
31 max_entries,
32 ttl: Duration::from_secs(ttl_hours * 3600),
33 }
34 }
35
36 pub fn with_24h_ttl(max_entries: usize) -> Self {
38 Self::new(max_entries, 24)
39 }
40
41 pub async fn get_hints(&self, file_path: &str, file_hash: u64) -> Option<Vec<SymbolHint>> {
43 let mut cache = self.cache.write().await;
44
45 if let Some(entry) = cache.get_mut(file_path) {
46 if entry.created_at.elapsed() < self.ttl && entry.file_hash == file_hash {
48 entry.access_count += 1;
49 debug!("Cache hit for {}: {} hints", file_path, entry.hints.len());
50 return Some(entry.hints.clone());
51 } else {
52 cache.remove(file_path);
54 debug!("Cache miss (stale) for {}", file_path);
55 }
56 } else {
57 debug!("Cache miss for {}", file_path);
58 }
59
60 None
61 }
62
63 pub async fn store_hints(&self, file_path: String, file_hash: u64, hints: Vec<SymbolHint>) {
65 let mut cache = self.cache.write().await;
66
67 if cache.len() >= self.max_entries {
69 self.evict_lru(&mut cache).await;
70 }
71
72 let entry = CacheEntry {
73 hints,
74 created_at: Instant::now(),
75 file_hash,
76 access_count: 1,
77 };
78
79 cache.insert(file_path.clone(), entry);
80 debug!("Cached hints for {}: {} entries", file_path, cache.len());
81 }
82
83 pub async fn invalidate(&self, file_path: &str) {
85 let mut cache = self.cache.write().await;
86 if cache.remove(file_path).is_some() {
87 debug!("Invalidated cache for {}", file_path);
88 }
89 }
90
91 pub async fn clear(&self) {
93 let mut cache = self.cache.write().await;
94 let count = cache.len();
95 cache.clear();
96 info!("Cleared cache: {} entries removed", count);
97 }
98
99 pub async fn stats(&self) -> CacheStats {
101 let cache = self.cache.read().await;
102 let now = Instant::now();
103
104 let mut total_access_count = 0;
105 let mut valid_entries = 0;
106 let mut stale_entries = 0;
107
108 for entry in cache.values() {
109 total_access_count += entry.access_count;
110 if entry.created_at.elapsed() < self.ttl {
111 valid_entries += 1;
112 } else {
113 stale_entries += 1;
114 }
115 }
116
117 CacheStats {
118 total_entries: cache.len(),
119 valid_entries,
120 stale_entries,
121 total_access_count,
122 }
123 }
124
125 async fn evict_lru(&self, cache: &mut HashMap<String, CacheEntry>) {
127 if let Some(lru_key) = cache
128 .iter()
129 .min_by_key(|(_, entry)| entry.access_count)
130 .map(|(k, _)| k.clone())
131 {
132 cache.remove(&lru_key);
133 debug!("Evicted LRU entry: {}", lru_key);
134 }
135 }
136
137 pub async fn cleanup_stale(&self) {
139 let mut cache = self.cache.write().await;
140 let initial_count = cache.len();
141
142 cache.retain(|_, entry| entry.created_at.elapsed() < self.ttl);
143
144 let removed_count = initial_count - cache.len();
145 if removed_count > 0 {
146 info!("Cleaned up {} stale cache entries", removed_count);
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct CacheStats {
153 pub total_entries: usize,
154 pub valid_entries: usize,
155 pub stale_entries: usize,
156 pub total_access_count: u64,
157}
158
159impl Default for HintCache {
160 fn default() -> Self {
161 Self::with_24h_ttl(10000) }
163}
164
165pub struct FileHasher;
167
168impl FileHasher {
169 pub fn hash_file_content(content: &[u8]) -> u64 {
171 use std::collections::hash_map::DefaultHasher;
172 use std::hash::{Hash, Hasher};
173
174 let mut hasher = DefaultHasher::new();
175 content.hash(&mut hasher);
176 hasher.finish()
177 }
178
179 pub async fn hash_file_metadata(path: &std::path::Path) -> Result<u64, std::io::Error> {
181 let metadata = tokio::fs::metadata(path).await?;
182
183 let mut hasher = std::collections::hash_map::DefaultHasher::new();
184 use std::hash::{Hash, Hasher};
185
186 metadata.len().hash(&mut hasher);
187 if let Ok(modified) = metadata.modified() {
188 if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
189 duration.as_secs().hash(&mut hasher);
190 }
191 }
192
193 Ok(hasher.finish())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::lsp::hint::SymbolHint;
201 use std::path::PathBuf;
202 use tokio::time::{sleep, Duration};
203
204 fn create_test_hint(name: &str) -> SymbolHint {
205 SymbolHint::new(
206 name.to_string(),
207 "function".to_string(),
208 "/test/file.rs".to_string(),
209 1,
210 1,
211 0.8,
212 )
213 }
214
215 #[tokio::test]
216 async fn test_hint_cache_new() {
217 let cache = HintCache::new(100, 1);
218
219 assert_eq!(cache.max_entries, 100);
220 assert_eq!(cache.ttl, Duration::from_secs(3600));
221 }
222
223 #[tokio::test]
224 async fn test_hint_cache_24h_ttl() {
225 let cache = HintCache::with_24h_ttl(50);
226
227 assert_eq!(cache.max_entries, 50);
228 assert_eq!(cache.ttl, Duration::from_secs(24 * 3600));
229 }
230
231 #[tokio::test]
232 async fn test_cache_store_and_get() {
233 let cache = HintCache::new(10, 1);
234 let hints = vec![
235 create_test_hint("function1"),
236 create_test_hint("function2"),
237 ];
238
239 cache.store_hints("test.rs".to_string(), 12345, hints.clone()).await;
241
242 let retrieved = cache.get_hints("test.rs", 12345).await;
244 assert!(retrieved.is_some());
245
246 let retrieved_hints = retrieved.unwrap();
247 assert_eq!(retrieved_hints.len(), 2);
248 assert_eq!(retrieved_hints[0].symbol_name, "function1");
249 assert_eq!(retrieved_hints[1].symbol_name, "function2");
250 }
251
252 #[tokio::test]
253 async fn test_cache_miss_different_hash() {
254 let cache = HintCache::new(10, 1);
255 let hints = vec![create_test_hint("function1")];
256
257 cache.store_hints("test.rs".to_string(), 12345, hints).await;
258
259 let retrieved = cache.get_hints("test.rs", 67890).await;
261 assert!(retrieved.is_none());
262 }
263
264 #[tokio::test]
265 async fn test_cache_miss_different_file() {
266 let cache = HintCache::new(10, 1);
267 let hints = vec![create_test_hint("function1")];
268
269 cache.store_hints("test.rs".to_string(), 12345, hints).await;
270
271 let retrieved = cache.get_hints("other.rs", 12345).await;
273 assert!(retrieved.is_none());
274 }
275
276 #[tokio::test]
277 async fn test_cache_eviction_by_size() {
278 let cache = HintCache::new(2, 1); let hints1 = vec![create_test_hint("func1")];
280 let hints2 = vec![create_test_hint("func2")];
281 let hints3 = vec![create_test_hint("func3")];
282
283 cache.store_hints("file1.rs".to_string(), 1, hints1).await;
285 cache.store_hints("file2.rs".to_string(), 2, hints2).await;
286
287 cache.store_hints("file3.rs".to_string(), 3, hints3).await;
289
290 let stats = cache.stats().await;
292 assert!(stats.total_entries <= 2);
293 }
294
295 #[tokio::test]
296 async fn test_cache_clear() {
297 let cache = HintCache::new(10, 1);
298 let hints = vec![create_test_hint("function1")];
299
300 cache.store_hints("test.rs".to_string(), 12345, hints).await;
301
302 let retrieved = cache.get_hints("test.rs", 12345).await;
304 assert!(retrieved.is_some());
305
306 cache.clear().await;
308
309 let cleared = cache.get_hints("test.rs", 12345).await;
311 assert!(cleared.is_none());
312 }
313
314 #[tokio::test]
315 async fn test_cache_invalidate_file() {
316 let cache = HintCache::new(10, 1);
317 let hints1 = vec![create_test_hint("func1")];
318 let hints2 = vec![create_test_hint("func2")];
319
320 cache.store_hints("file1.rs".to_string(), 1, hints1).await;
321 cache.store_hints("file2.rs".to_string(), 2, hints2).await;
322
323 cache.invalidate("file1.rs").await;
325
326 assert!(cache.get_hints("file1.rs", 1).await.is_none());
328 assert!(cache.get_hints("file2.rs", 2).await.is_some());
329 }
330
331 #[tokio::test]
332 async fn test_cache_stats() {
333 let cache = HintCache::new(10, 1);
334 let hints = vec![create_test_hint("function1")];
335
336 let initial_stats = cache.stats().await;
338 assert_eq!(initial_stats.total_entries, 0);
339
340 cache.store_hints("test.rs".to_string(), 12345, hints).await;
342
343 let after_store = cache.stats().await;
345 assert_eq!(after_store.total_entries, 1);
346 }
347
348 #[tokio::test]
349 async fn test_concurrent_cache_access() {
350 let cache = Arc::new(HintCache::new(10, 1));
351 let hints = vec![create_test_hint("function1")];
352
353 cache.store_hints("test.rs".to_string(), 12345, hints).await;
354
355 let mut handles = vec![];
357 for i in 0..10 {
358 let cache_clone = cache.clone();
359 let handle = tokio::spawn(async move {
360 let result = cache_clone.get_hints("test.rs", 12345).await;
361 assert!(result.is_some(), "Read {} should succeed", i);
362 });
363 handles.push(handle);
364 }
365
366 for handle in handles {
368 handle.await.expect("Task should complete successfully");
369 }
370 }
371
372 #[test]
373 fn test_file_hasher_consistency() {
374 let content1 = b"hello world";
375 let content2 = b"hello world";
376 let content3 = b"hello world!";
377
378 assert_eq!(
380 FileHasher::hash_file_content(content1),
381 FileHasher::hash_file_content(content2)
382 );
383
384 assert_ne!(
386 FileHasher::hash_file_content(content1),
387 FileHasher::hash_file_content(content3)
388 );
389 }
390
391 #[test]
392 fn test_file_hasher_empty_content() {
393 let empty = b"";
394 let hash = FileHasher::hash_file_content(empty);
395
396 assert!(hash != 0); }
399
400 #[test]
401 fn test_cache_stats_default() {
402 let stats = CacheStats {
403 total_entries: 0,
404 valid_entries: 0,
405 stale_entries: 0,
406 total_access_count: 0,
407 };
408
409 assert_eq!(stats.total_entries, 0);
410 assert_eq!(stats.valid_entries, 0);
411 assert_eq!(stats.stale_entries, 0);
412 assert_eq!(stats.total_access_count, 0);
413 }
414
415 #[test]
416 fn test_cache_entry_creation() {
417 let hints = vec![create_test_hint("test_func")];
418 let entry = CacheEntry {
419 hints: hints.clone(),
420 created_at: Instant::now(),
421 file_hash: 12345,
422 access_count: 0,
423 };
424
425 assert_eq!(entry.hints.len(), 1);
426 assert_eq!(entry.file_hash, 12345);
427 assert_eq!(entry.access_count, 0);
428 assert_eq!(entry.hints[0].symbol_name, "test_func");
429 }
430
431 #[tokio::test]
432 async fn test_default_cache_configuration() {
433 let cache = HintCache::default();
434
435 assert_eq!(cache.ttl, Duration::from_secs(24 * 3600));
437 assert_eq!(cache.max_entries, 10000);
438 }
439}