ricecoder_lsp/
cache.rs

1//! Caching layer for semantic analysis and AST parsing
2//!
3//! This module provides caching mechanisms to improve performance by:
4//! - Caching parsed ASTs for unchanged documents
5//! - Caching semantic analysis results
6//! - Caching symbol indexes
7//! - Tracking cache hit rates and performance metrics
8
9use crate::types::SemanticInfo;
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12use std::time::{SystemTime, UNIX_EPOCH};
13use tracing::debug;
14
15/// Cache entry with timestamp and content hash
16#[derive(Debug, Clone)]
17struct CacheEntry<T> {
18    /// Cached value
19    value: T,
20    /// Timestamp when entry was created
21    timestamp: u64,
22    /// Hash of the input that produced this value
23    input_hash: u64,
24}
25
26/// Performance metrics for caching
27#[derive(Debug, Clone, Default)]
28pub struct CacheMetrics {
29    /// Total cache hits
30    pub hits: u64,
31    /// Total cache misses
32    pub misses: u64,
33    /// Total cache invalidations
34    pub invalidations: u64,
35    /// Average analysis time in milliseconds
36    pub avg_analysis_time_ms: f64,
37}
38
39impl CacheMetrics {
40    /// Calculate hit rate as percentage
41    pub fn hit_rate(&self) -> f64 {
42        let total = self.hits + self.misses;
43        if total == 0 {
44            0.0
45        } else {
46            (self.hits as f64 / total as f64) * 100.0
47        }
48    }
49}
50
51/// Semantic analysis cache
52pub struct SemanticCache {
53    /// Cache entries by document URI
54    entries: Arc<RwLock<HashMap<String, CacheEntry<SemanticInfo>>>>,
55    /// Performance metrics
56    metrics: Arc<RwLock<CacheMetrics>>,
57    /// Maximum cache size in bytes (approximate)
58    max_size: usize,
59    /// Current cache size in bytes (approximate)
60    current_size: Arc<RwLock<usize>>,
61}
62
63impl SemanticCache {
64    /// Create a new semantic cache with default size (100MB)
65    pub fn new() -> Self {
66        Self::with_size(100 * 1024 * 1024)
67    }
68
69    /// Create a new semantic cache with specified size
70    pub fn with_size(max_size: usize) -> Self {
71        Self {
72            entries: Arc::new(RwLock::new(HashMap::new())),
73            metrics: Arc::new(RwLock::new(CacheMetrics::default())),
74            max_size,
75            current_size: Arc::new(RwLock::new(0)),
76        }
77    }
78
79    /// Get cached semantic information
80    pub fn get(&self, uri: &str, input_hash: u64) -> Option<SemanticInfo> {
81        let entries = self.entries.read().unwrap();
82
83        if let Some(entry) = entries.get(uri) {
84            if entry.input_hash == input_hash {
85                debug!("Cache hit for {}", uri);
86                let mut metrics = self.metrics.write().unwrap();
87                metrics.hits += 1;
88                return Some(entry.value.clone());
89            }
90        }
91
92        let mut metrics = self.metrics.write().unwrap();
93        metrics.misses += 1;
94        None
95    }
96
97    /// Store semantic information in cache
98    pub fn put(&self, uri: String, input_hash: u64, value: SemanticInfo) {
99        let estimated_size = self.estimate_size(&value);
100
101        // Check if we need to evict entries
102        let mut current_size = self.current_size.write().unwrap();
103        if *current_size + estimated_size > self.max_size {
104            self.evict_oldest();
105            *current_size = self.calculate_total_size();
106        }
107
108        let timestamp = SystemTime::now()
109            .duration_since(UNIX_EPOCH)
110            .unwrap()
111            .as_secs();
112
113        let entry = CacheEntry {
114            value,
115            timestamp,
116            input_hash,
117        };
118
119        let mut entries = self.entries.write().unwrap();
120        entries.insert(uri.clone(), entry);
121        *current_size += estimated_size;
122
123        debug!("Cached semantic info for {}", uri);
124    }
125
126    /// Invalidate cache entry for a document
127    pub fn invalidate(&self, uri: &str) {
128        let mut entries = self.entries.write().unwrap();
129        if entries.remove(uri).is_some() {
130            let mut metrics = self.metrics.write().unwrap();
131            metrics.invalidations += 1;
132            debug!("Invalidated cache for {}", uri);
133        }
134    }
135
136    /// Clear all cache entries
137    pub fn clear(&self) {
138        let mut entries = self.entries.write().unwrap();
139        entries.clear();
140        let mut current_size = self.current_size.write().unwrap();
141        *current_size = 0;
142        debug!("Cache cleared");
143    }
144
145    /// Get cache metrics
146    pub fn metrics(&self) -> CacheMetrics {
147        self.metrics.read().unwrap().clone()
148    }
149
150    /// Estimate size of semantic info in bytes
151    fn estimate_size(&self, info: &SemanticInfo) -> usize {
152        // Rough estimation: 200 bytes per symbol + 100 bytes per import/definition/reference
153        let symbols_size = info.symbols.len() * 200;
154        let imports_size = info.imports.len() * 100;
155        let definitions_size = info.definitions.len() * 100;
156        let references_size = info.references.len() * 100;
157
158        symbols_size + imports_size + definitions_size + references_size
159    }
160
161    /// Calculate total cache size
162    fn calculate_total_size(&self) -> usize {
163        let entries = self.entries.read().unwrap();
164        entries
165            .values()
166            .map(|entry| self.estimate_size(&entry.value))
167            .sum()
168    }
169
170    /// Evict oldest cache entry
171    fn evict_oldest(&self) {
172        let mut entries = self.entries.write().unwrap();
173
174        if let Some((oldest_uri, _)) = entries
175            .iter()
176            .min_by_key(|(_, entry)| entry.timestamp)
177            .map(|(uri, entry)| (uri.clone(), entry.timestamp))
178        {
179            entries.remove(&oldest_uri);
180            debug!("Evicted oldest cache entry: {}", oldest_uri);
181        }
182    }
183}
184
185impl Default for SemanticCache {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191/// AST cache for parsed syntax trees
192pub struct AstCache {
193    /// Cache entries by document URI
194    entries: Arc<RwLock<HashMap<String, CacheEntry<String>>>>,
195    /// Performance metrics
196    metrics: Arc<RwLock<CacheMetrics>>,
197}
198
199impl AstCache {
200    /// Create a new AST cache
201    pub fn new() -> Self {
202        Self {
203            entries: Arc::new(RwLock::new(HashMap::new())),
204            metrics: Arc::new(RwLock::new(CacheMetrics::default())),
205        }
206    }
207
208    /// Get cached AST
209    pub fn get(&self, uri: &str, input_hash: u64) -> Option<String> {
210        let entries = self.entries.read().unwrap();
211
212        if let Some(entry) = entries.get(uri) {
213            if entry.input_hash == input_hash {
214                debug!("AST cache hit for {}", uri);
215                let mut metrics = self.metrics.write().unwrap();
216                metrics.hits += 1;
217                return Some(entry.value.clone());
218            }
219        }
220
221        let mut metrics = self.metrics.write().unwrap();
222        metrics.misses += 1;
223        None
224    }
225
226    /// Store AST in cache
227    pub fn put(&self, uri: String, input_hash: u64, value: String) {
228        let timestamp = SystemTime::now()
229            .duration_since(UNIX_EPOCH)
230            .unwrap()
231            .as_secs();
232
233        let entry = CacheEntry {
234            value,
235            timestamp,
236            input_hash,
237        };
238
239        let mut entries = self.entries.write().unwrap();
240        entries.insert(uri.clone(), entry);
241
242        debug!("Cached AST for {}", uri);
243    }
244
245    /// Invalidate cache entry for a document
246    pub fn invalidate(&self, uri: &str) {
247        let mut entries = self.entries.write().unwrap();
248        if entries.remove(uri).is_some() {
249            let mut metrics = self.metrics.write().unwrap();
250            metrics.invalidations += 1;
251            debug!("Invalidated AST cache for {}", uri);
252        }
253    }
254
255    /// Clear all cache entries
256    pub fn clear(&self) {
257        let mut entries = self.entries.write().unwrap();
258        entries.clear();
259        debug!("AST cache cleared");
260    }
261
262    /// Get cache metrics
263    pub fn metrics(&self) -> CacheMetrics {
264        self.metrics.read().unwrap().clone()
265    }
266}
267
268impl Default for AstCache {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274/// Type alias for symbol index cache entries
275type SymbolIndexEntries = Arc<RwLock<HashMap<String, CacheEntry<HashMap<String, usize>>>>>;
276
277/// Symbol index cache
278pub struct SymbolIndexCache {
279    /// Cache entries by document URI
280    entries: SymbolIndexEntries,
281    /// Performance metrics
282    metrics: Arc<RwLock<CacheMetrics>>,
283}
284
285impl SymbolIndexCache {
286    /// Create a new symbol index cache
287    pub fn new() -> Self {
288        Self {
289            entries: Arc::new(RwLock::new(HashMap::new())),
290            metrics: Arc::new(RwLock::new(CacheMetrics::default())),
291        }
292    }
293
294    /// Get cached symbol index
295    pub fn get(&self, uri: &str, input_hash: u64) -> Option<HashMap<String, usize>> {
296        let entries = self.entries.read().unwrap();
297
298        if let Some(entry) = entries.get(uri) {
299            if entry.input_hash == input_hash {
300                debug!("Symbol index cache hit for {}", uri);
301                let mut metrics = self.metrics.write().unwrap();
302                metrics.hits += 1;
303                return Some(entry.value.clone());
304            }
305        }
306
307        let mut metrics = self.metrics.write().unwrap();
308        metrics.misses += 1;
309        None
310    }
311
312    /// Store symbol index in cache
313    pub fn put(&self, uri: String, input_hash: u64, value: HashMap<String, usize>) {
314        let timestamp = SystemTime::now()
315            .duration_since(UNIX_EPOCH)
316            .unwrap()
317            .as_secs();
318
319        let entry = CacheEntry {
320            value,
321            timestamp,
322            input_hash,
323        };
324
325        let mut entries = self.entries.write().unwrap();
326        entries.insert(uri.clone(), entry);
327
328        debug!("Cached symbol index for {}", uri);
329    }
330
331    /// Invalidate cache entry for a document
332    pub fn invalidate(&self, uri: &str) {
333        let mut entries = self.entries.write().unwrap();
334        if entries.remove(uri).is_some() {
335            let mut metrics = self.metrics.write().unwrap();
336            metrics.invalidations += 1;
337            debug!("Invalidated symbol index cache for {}", uri);
338        }
339    }
340
341    /// Clear all cache entries
342    pub fn clear(&self) {
343        let mut entries = self.entries.write().unwrap();
344        entries.clear();
345        debug!("Symbol index cache cleared");
346    }
347
348    /// Get cache metrics
349    pub fn metrics(&self) -> CacheMetrics {
350        self.metrics.read().unwrap().clone()
351    }
352}
353
354impl Default for SymbolIndexCache {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360/// Compute hash of input string
361pub fn hash_input(input: &str) -> u64 {
362    use std::collections::hash_map::DefaultHasher;
363    use std::hash::{Hash, Hasher};
364
365    let mut hasher = DefaultHasher::new();
366    input.hash(&mut hasher);
367    hasher.finish()
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use crate::types::{Position, Range, Symbol, SymbolKind};
374
375    #[test]
376    fn test_semantic_cache_hit() {
377        let cache = SemanticCache::new();
378        let mut info = SemanticInfo::new();
379        info.symbols.push(Symbol::new(
380            "test".to_string(),
381            SymbolKind::Function,
382            Range::new(Position::new(0, 0), Position::new(0, 4)),
383        ));
384
385        let hash = hash_input("test code");
386        cache.put("file://test.rs".to_string(), hash, info.clone());
387
388        let cached = cache.get("file://test.rs", hash);
389        assert!(cached.is_some());
390        assert_eq!(cached.unwrap().symbols.len(), 1);
391
392        let metrics = cache.metrics();
393        assert_eq!(metrics.hits, 1);
394        assert_eq!(metrics.misses, 0);
395    }
396
397    #[test]
398    fn test_semantic_cache_miss() {
399        let cache = SemanticCache::new();
400        let info = SemanticInfo::new();
401
402        let hash = hash_input("test code");
403        cache.put("file://test.rs".to_string(), hash, info);
404
405        // Try to get with different hash
406        let cached = cache.get("file://test.rs", hash + 1);
407        assert!(cached.is_none());
408
409        let metrics = cache.metrics();
410        assert_eq!(metrics.hits, 0);
411        assert_eq!(metrics.misses, 1);
412    }
413
414    #[test]
415    fn test_semantic_cache_invalidation() {
416        let cache = SemanticCache::new();
417        let info = SemanticInfo::new();
418
419        let hash = hash_input("test code");
420        cache.put("file://test.rs".to_string(), hash, info);
421
422        cache.invalidate("file://test.rs");
423
424        let cached = cache.get("file://test.rs", hash);
425        assert!(cached.is_none());
426
427        let metrics = cache.metrics();
428        assert_eq!(metrics.invalidations, 1);
429    }
430
431    #[test]
432    fn test_ast_cache() {
433        let cache = AstCache::new();
434        let ast = "fn main() {}".to_string();
435
436        let hash = hash_input("fn main() {}");
437        cache.put("file://test.rs".to_string(), hash, ast.clone());
438
439        let cached = cache.get("file://test.rs", hash);
440        assert_eq!(cached, Some(ast));
441    }
442
443    #[test]
444    fn test_symbol_index_cache() {
445        let cache = SymbolIndexCache::new();
446        let mut index = HashMap::new();
447        index.insert("main".to_string(), 0);
448
449        let hash = hash_input("fn main() {}");
450        cache.put("file://test.rs".to_string(), hash, index.clone());
451
452        let cached = cache.get("file://test.rs", hash);
453        assert_eq!(cached, Some(index));
454    }
455
456    #[test]
457    fn test_cache_metrics_hit_rate() {
458        let metrics = CacheMetrics {
459            hits: 80,
460            misses: 20,
461            invalidations: 0,
462            avg_analysis_time_ms: 0.0,
463        };
464
465        assert_eq!(metrics.hit_rate(), 80.0);
466    }
467
468    #[test]
469    fn test_cache_clear() {
470        let cache = SemanticCache::new();
471        let info = SemanticInfo::new();
472
473        let hash = hash_input("test code");
474        cache.put("file://test.rs".to_string(), hash, info);
475
476        cache.clear();
477
478        let cached = cache.get("file://test.rs", hash);
479        assert!(cached.is_none());
480    }
481}