Skip to main content

vtcode_core/tools/
cache.rs

1//! Caching system for tool results
2
3use super::types::{EnhancedCacheEntry, EnhancedCacheStats};
4use once_cell::sync::Lazy;
5use quick_cache::sync::Cache;
6use serde_json::Value;
7use std::future::Future;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::time::Duration;
11
12use parking_lot::RwLock;
13
14use crate::cache::estimate_json_size;
15use vtcode_config::FileReadCacheConfig;
16
17/// Global file cache instance
18pub static FILE_CACHE: Lazy<FileCache> = Lazy::new(|| FileCache::new(1000));
19
20static FILE_READ_CACHE_CONFIG: Lazy<RwLock<FileReadCacheConfig>> =
21    Lazy::new(|| RwLock::new(FileReadCacheConfig::default()));
22
23/// Enhanced file cache with quick-cache for high-performance caching
24///
25/// Uses a `parking_lot::Mutex` for stats access — the critical sections contain
26/// no `.await`, so the cheaper sync mutex avoids async-mutex overhead on the
27/// hot get/put path.
28/// Stores `Arc<Value>` internally for zero-copy cache hits.
29/// See: <https://ratatui.rs/faq/>
30pub struct FileCache {
31    file_cache: Arc<Cache<String, EnhancedCacheEntry<Arc<Value>>>>,
32    directory_cache: Arc<Cache<String, EnhancedCacheEntry<Arc<Value>>>>,
33    stats: Arc<parking_lot::Mutex<EnhancedCacheStats>>,
34    max_size_bytes: AtomicUsize,
35    ttl_millis: AtomicU64,
36}
37
38impl FileCache {
39    pub fn new(capacity: usize) -> Self {
40        Self {
41            file_cache: Arc::new(Cache::new(capacity)),
42            directory_cache: Arc::new(Cache::new(capacity / 2)),
43            stats: Arc::new(parking_lot::Mutex::new(EnhancedCacheStats::default())),
44            max_size_bytes: AtomicUsize::new(50 * 1024 * 1024), // 50MB default
45            ttl_millis: AtomicU64::new(300_000),                // 5 minutes default
46        }
47    }
48
49    #[inline]
50    fn ttl(&self) -> Duration {
51        Duration::from_millis(self.ttl_millis.load(Ordering::Relaxed))
52    }
53
54    #[inline]
55    fn max_size_bytes(&self) -> usize {
56        self.max_size_bytes.load(Ordering::Relaxed)
57    }
58
59    /// Get cached file content (clones the value for backwards compatibility)
60    pub async fn get_file(&self, key: &str) -> Option<Value> {
61        self.get_file_arc(key).await.map(|arc| (*arc).clone())
62    }
63
64    /// Get cached file content as Arc for zero-copy access
65    pub async fn get_file_arc(&self, key: &str) -> Option<Arc<Value>> {
66        let mut stats = self.stats.lock();
67
68        if let Some(entry) = self.file_cache.get(key) {
69            // Check if entry is still valid
70            if entry.timestamp.elapsed() < self.ttl() {
71                // Note: quick-cache handles access tracking automatically
72                stats.hits += 1;
73                return Some(Arc::clone(&entry.data));
74            } else {
75                // Entry expired, remove it
76                let size = entry.size_bytes;
77                self.file_cache.remove(key);
78                stats.expired_evictions += 1;
79                stats.total_size_bytes = stats.total_size_bytes.saturating_sub(size);
80                stats.file_size_bytes = stats.file_size_bytes.saturating_sub(size);
81            }
82        }
83
84        stats.misses += 1;
85        None
86    }
87
88    /// Calculate byte size of a JSON value for cache tracking.
89    /// Walks the Value tree without allocating, unlike the previous
90    /// implementation that serialized to a temporary String.
91    #[inline]
92    fn estimate_value_size(value: &Value) -> usize {
93        estimate_json_size(value) as usize
94    }
95
96    /// Cache file content
97    pub fn put_file(&self, key: String, value: Value) -> impl Future<Output = ()> + '_ {
98        self.put_file_arc(key, Arc::new(value))
99    }
100
101    /// Cache file content with pre-wrapped Arc for zero-copy insertion
102    pub async fn put_file_arc(&self, key: String, value: Arc<Value>) {
103        let size_bytes = Self::estimate_value_size(&value);
104        let entry = EnhancedCacheEntry::new(value, size_bytes);
105
106        self.file_cache.insert(key, entry);
107
108        let mut stats = self.stats.lock();
109        stats.file_entries = self.file_cache.len();
110        stats.entries = stats.file_entries + stats.directory_entries;
111        stats.file_size_bytes += size_bytes;
112        stats.total_size_bytes = stats.file_size_bytes + stats.directory_size_bytes;
113    }
114
115    /// Get cached directory listing (clones for backwards compatibility)
116    pub async fn get_directory(&self, key: &str) -> Option<Value> {
117        self.get_directory_arc(key).await.map(|arc| (*arc).clone())
118    }
119
120    /// Get cached directory listing as Arc for zero-copy access
121    pub async fn get_directory_arc(&self, key: &str) -> Option<Arc<Value>> {
122        let mut stats = self.stats.lock();
123
124        if let Some(entry) = self.directory_cache.get(key) {
125            if entry.timestamp.elapsed() < self.ttl() {
126                stats.hits += 1;
127                return Some(Arc::clone(&entry.data));
128            } else {
129                let size = entry.size_bytes;
130                self.directory_cache.remove(key);
131                stats.expired_evictions += 1;
132                stats.total_size_bytes = stats.total_size_bytes.saturating_sub(size);
133                stats.directory_size_bytes = stats.directory_size_bytes.saturating_sub(size);
134            }
135        }
136
137        stats.misses += 1;
138        None
139    }
140
141    /// Cache directory listing
142    pub fn put_directory(&self, key: String, value: Value) -> impl Future<Output = ()> + '_ {
143        self.put_directory_arc(key, Arc::new(value))
144    }
145
146    /// Cache directory listing with pre-wrapped Arc
147    pub async fn put_directory_arc(&self, key: String, value: Arc<Value>) {
148        let size_bytes = Self::estimate_value_size(&value);
149        let entry = EnhancedCacheEntry::new(value, size_bytes);
150
151        self.directory_cache.insert(key, entry);
152
153        let mut stats = self.stats.lock();
154        stats.directory_entries = self.directory_cache.len();
155        stats.entries = stats.file_entries + stats.directory_entries;
156        stats.directory_size_bytes += size_bytes;
157        stats.total_size_bytes = stats.file_size_bytes + stats.directory_size_bytes;
158    }
159
160    /// Get cache statistics
161    pub async fn stats(&self) -> EnhancedCacheStats {
162        self.stats.lock().clone()
163    }
164
165    /// Clear all caches
166    pub async fn clear(&self) {
167        self.file_cache.clear();
168        self.directory_cache.clear();
169        *self.stats.lock() = EnhancedCacheStats::default();
170    }
171
172    /// Get cache capacity information
173    pub fn capacity(&self) -> (usize, usize) {
174        (
175            self.file_cache.capacity().try_into().unwrap_or(0),
176            self.directory_cache.capacity().try_into().unwrap_or(0),
177        )
178    }
179
180    /// Get current cache size
181    pub fn len(&self) -> (usize, usize) {
182        (self.file_cache.len(), self.directory_cache.len())
183    }
184
185    /// Check memory pressure and enforce limits with tiered eviction
186    pub async fn check_pressure_and_evict(&self) {
187        let mut stats = self.stats.lock();
188
189        let current_size = stats.total_size_bytes;
190        let max_size = self.max_size_bytes();
191
192        if current_size > max_size {
193            // Tier 1: Clear directory cache first (cheaper to rebuild)
194            self.directory_cache.clear();
195            stats.directory_entries = 0;
196            stats.directory_size_bytes = 0;
197
198            // Recalculate total
199            stats.total_size_bytes = stats.file_size_bytes;
200            stats.entries = stats.file_entries;
201
202            // If still very over limit (e.g. 150%), clear everything
203            if stats.total_size_bytes as f64 > max_size as f64 * 1.5 {
204                self.file_cache.clear();
205                stats.file_entries = 0;
206                stats.file_size_bytes = 0;
207                stats.total_size_bytes = 0;
208                stats.entries = 0;
209                stats.memory_evictions += 1;
210            } else if stats.total_size_bytes > max_size {
211                // Moderately over: clear file cache as safety measure
212                self.file_cache.clear();
213                stats.file_entries = 0;
214                stats.file_size_bytes = 0;
215                stats.total_size_bytes = 0;
216                stats.entries = 0;
217                stats.memory_evictions += 1;
218            }
219        } else if current_size as f64 > max_size as f64 * 0.9 {
220            // Tier 3: Soft limit - proactive directory pruning
221            self.directory_cache.clear();
222            stats.directory_entries = 0;
223            stats.directory_size_bytes = 0;
224            stats.total_size_bytes = stats.file_size_bytes;
225            stats.entries = stats.file_entries;
226        }
227    }
228
229    /// Set explicit memory limit in bytes
230    pub fn set_capacity_limit(&self, max_bytes: usize) {
231        self.max_size_bytes.store(max_bytes, Ordering::Relaxed);
232    }
233
234    /// Update cache policy from configuration
235    pub fn apply_read_cache_config(&self, config: &FileReadCacheConfig) {
236        self.max_size_bytes.store(config.max_size_bytes, Ordering::Relaxed);
237        self.ttl_millis.store(config.ttl_secs.saturating_mul(1000), Ordering::Relaxed);
238    }
239}
240
241/// Configure global file cache from optimization settings.
242pub fn configure_file_cache(config: &FileReadCacheConfig) {
243    *FILE_READ_CACHE_CONFIG.write() = config.clone();
244    FILE_CACHE.apply_read_cache_config(config);
245}
246
247/// Get a clone of the current file read cache config
248pub fn file_read_cache_config() -> FileReadCacheConfig {
249    FILE_READ_CACHE_CONFIG.read().clone()
250}