code_analyze_mcp/
cache.rs1use crate::analyze::{AnalysisOutput, FileAnalysisOutput};
7use crate::traversal::WalkEntry;
8use crate::types::AnalysisMode;
9use lru::LruCache;
10use std::fs;
11use std::num::NonZeroUsize;
12use std::path::PathBuf;
13use std::sync::{Arc, Mutex};
14use std::time::SystemTime;
15use tracing::{debug, instrument};
16
17const DIR_CACHE_CAPACITY: usize = 20;
18
19#[derive(Debug, Clone, Eq, PartialEq, Hash)]
21pub struct CacheKey {
22 pub path: PathBuf,
23 pub modified: SystemTime,
24 pub mode: AnalysisMode,
25}
26
27#[derive(Debug, Clone, Eq, PartialEq, Hash)]
29pub struct DirectoryCacheKey {
30 files: Vec<(PathBuf, SystemTime)>,
31 mode: AnalysisMode,
32 max_depth: Option<u32>,
33}
34
35impl DirectoryCacheKey {
36 pub fn from_entries(entries: &[WalkEntry], max_depth: Option<u32>, mode: AnalysisMode) -> Self {
39 let mut files: Vec<(PathBuf, SystemTime)> = entries
40 .iter()
41 .map(|e| {
42 let mtime = fs::metadata(&e.path)
43 .and_then(|m| m.modified())
44 .unwrap_or(SystemTime::UNIX_EPOCH);
45 (e.path.clone(), mtime)
46 })
47 .collect();
48 files.sort_by(|a, b| a.0.cmp(&b.0));
49 Self {
50 files,
51 mode,
52 max_depth,
53 }
54 }
55}
56
57fn lock_or_recover<K, V, T, F>(mutex: &Mutex<LruCache<K, V>>, capacity: usize, recovery: F) -> T
60where
61 K: std::hash::Hash + Eq,
62 F: FnOnce(&mut LruCache<K, V>) -> T,
63{
64 match mutex.lock() {
65 Ok(mut guard) => recovery(&mut guard),
66 Err(poisoned) => {
67 let cache_size = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap());
68 let new_cache = LruCache::new(cache_size);
69 let mut guard = poisoned.into_inner();
70 *guard = new_cache;
71 recovery(&mut guard)
72 }
73 }
74}
75
76pub struct AnalysisCache {
78 file_capacity: usize,
79 cache: Arc<Mutex<LruCache<CacheKey, Arc<FileAnalysisOutput>>>>,
80 directory_cache: Arc<Mutex<LruCache<DirectoryCacheKey, Arc<AnalysisOutput>>>>,
81}
82
83impl AnalysisCache {
84 pub fn new(capacity: usize) -> Self {
86 let file_capacity = capacity.max(1);
87 let cache_size = NonZeroUsize::new(file_capacity).unwrap();
88 let dir_cache_size = NonZeroUsize::new(DIR_CACHE_CAPACITY).unwrap();
89 Self {
90 file_capacity,
91 cache: Arc::new(Mutex::new(LruCache::new(cache_size))),
92 directory_cache: Arc::new(Mutex::new(LruCache::new(dir_cache_size))),
93 }
94 }
95
96 #[instrument(skip(self), fields(path = ?key.path))]
98 pub fn get(&self, key: &CacheKey) -> Option<Arc<FileAnalysisOutput>> {
99 lock_or_recover(&self.cache, self.file_capacity, |guard| {
100 let result = guard.get(key).cloned();
101 let cache_size = guard.len();
102 match result {
103 Some(v) => {
104 debug!(cache_event = "hit", cache_size = cache_size, path = ?key.path);
105 Some(v)
106 }
107 None => {
108 debug!(cache_event = "miss", cache_size = cache_size, path = ?key.path);
109 None
110 }
111 }
112 })
113 }
114
115 #[instrument(skip(self, value), fields(path = ?key.path))]
117 pub fn put(&self, key: CacheKey, value: Arc<FileAnalysisOutput>) {
118 lock_or_recover(&self.cache, self.file_capacity, |guard| {
119 let push_result = guard.push(key.clone(), value);
120 let cache_size = guard.len();
121 match push_result {
122 None => {
123 debug!(cache_event = "insert", cache_size = cache_size, path = ?key.path);
124 }
125 Some((returned_key, _)) => {
126 if returned_key == key {
127 debug!(cache_event = "update", cache_size = cache_size, path = ?key.path);
128 } else {
129 debug!(cache_event = "eviction", cache_size = cache_size, path = ?key.path, evicted_path = ?returned_key.path);
130 }
131 }
132 }
133 });
134 }
135
136 #[instrument(skip(self))]
138 pub fn get_directory(&self, key: &DirectoryCacheKey) -> Option<Arc<AnalysisOutput>> {
139 lock_or_recover(&self.directory_cache, DIR_CACHE_CAPACITY, |guard| {
140 let result = guard.get(key).cloned();
141 let cache_size = guard.len();
142 match result {
143 Some(v) => {
144 debug!(cache_event = "hit", cache_size = cache_size);
145 Some(v)
146 }
147 None => {
148 debug!(cache_event = "miss", cache_size = cache_size);
149 None
150 }
151 }
152 })
153 }
154
155 #[instrument(skip(self, value))]
157 pub fn put_directory(&self, key: DirectoryCacheKey, value: Arc<AnalysisOutput>) {
158 lock_or_recover(&self.directory_cache, DIR_CACHE_CAPACITY, |guard| {
159 let push_result = guard.push(key.clone(), value);
160 let cache_size = guard.len();
161 match push_result {
162 None => {
163 debug!(cache_event = "insert", cache_size = cache_size);
164 }
165 Some((_, _)) => {
166 debug!(cache_event = "eviction", cache_size = cache_size);
167 }
168 }
169 });
170 }
171}
172
173impl Clone for AnalysisCache {
174 fn clone(&self) -> Self {
175 Self {
176 file_capacity: self.file_capacity,
177 cache: Arc::clone(&self.cache),
178 directory_cache: Arc::clone(&self.directory_cache),
179 }
180 }
181}