1use crate::analyze::{AnalysisOutput, FileAnalysisOutput};
7use crate::traversal::WalkEntry;
8use crate::types::AnalysisMode;
9use lru::LruCache;
10use rayon::prelude::*;
11use std::fs;
12use std::num::NonZeroUsize;
13use std::path::PathBuf;
14use std::sync::{Arc, Mutex};
15use std::time::SystemTime;
16use tracing::{debug, instrument};
17
18const DIR_CACHE_CAPACITY: usize = 20;
19
20#[derive(Debug, Clone, Eq, PartialEq, Hash)]
22pub struct CacheKey {
23 pub path: PathBuf,
24 pub modified: SystemTime,
25 pub mode: AnalysisMode,
26}
27
28#[derive(Debug, Clone, Eq, PartialEq, Hash)]
30pub struct DirectoryCacheKey {
31 files: Vec<(PathBuf, SystemTime)>,
32 mode: AnalysisMode,
33 max_depth: Option<u32>,
34}
35
36impl DirectoryCacheKey {
37 #[must_use]
42 pub fn from_entries(entries: &[WalkEntry], max_depth: Option<u32>, mode: AnalysisMode) -> Self {
43 let mut files: Vec<(PathBuf, SystemTime)> = entries
44 .par_iter()
45 .filter(|e| !e.is_dir)
46 .map(|e| {
47 let mtime = fs::metadata(&e.path)
48 .and_then(|m| m.modified())
49 .unwrap_or(SystemTime::UNIX_EPOCH);
50 (e.path.clone(), mtime)
51 })
52 .collect();
53 files.sort_by(|a, b| a.0.cmp(&b.0));
54 Self {
55 files,
56 mode,
57 max_depth,
58 }
59 }
60}
61
62fn lock_or_recover<K, V, T, F>(mutex: &Mutex<LruCache<K, V>>, capacity: usize, recovery: F) -> T
65where
66 K: std::hash::Hash + Eq,
67 F: FnOnce(&mut LruCache<K, V>) -> T,
68{
69 match mutex.lock() {
70 Ok(mut guard) => recovery(&mut guard),
71 Err(poisoned) => {
72 let cache_size = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap());
73 let new_cache = LruCache::new(cache_size);
74 let mut guard = poisoned.into_inner();
75 *guard = new_cache;
76 recovery(&mut guard)
77 }
78 }
79}
80
81pub struct AnalysisCache {
83 file_capacity: usize,
84 cache: Arc<Mutex<LruCache<CacheKey, Arc<FileAnalysisOutput>>>>,
85 directory_cache: Arc<Mutex<LruCache<DirectoryCacheKey, Arc<AnalysisOutput>>>>,
86}
87
88impl AnalysisCache {
89 #[must_use]
91 pub fn new(capacity: usize) -> Self {
92 let file_capacity = capacity.max(1);
93 let cache_size = NonZeroUsize::new(file_capacity).unwrap();
94 let dir_cache_size = NonZeroUsize::new(DIR_CACHE_CAPACITY).unwrap();
95 Self {
96 file_capacity,
97 cache: Arc::new(Mutex::new(LruCache::new(cache_size))),
98 directory_cache: Arc::new(Mutex::new(LruCache::new(dir_cache_size))),
99 }
100 }
101
102 #[instrument(skip(self), fields(path = ?key.path))]
104 pub fn get(&self, key: &CacheKey) -> Option<Arc<FileAnalysisOutput>> {
105 lock_or_recover(&self.cache, self.file_capacity, |guard| {
106 let result = guard.get(key).cloned();
107 let cache_size = guard.len();
108 if let Some(v) = result {
109 debug!(cache_event = "hit", cache_size = cache_size, path = ?key.path);
110 Some(v)
111 } else {
112 debug!(cache_event = "miss", cache_size = cache_size, path = ?key.path);
113 None
114 }
115 })
116 }
117
118 #[instrument(skip(self, value), fields(path = ?key.path))]
120 #[allow(clippy::needless_pass_by_value)]
122 pub fn put(&self, key: CacheKey, value: Arc<FileAnalysisOutput>) {
123 lock_or_recover(&self.cache, self.file_capacity, |guard| {
124 let push_result = guard.push(key.clone(), value);
125 let cache_size = guard.len();
126 match push_result {
127 None => {
128 debug!(cache_event = "insert", cache_size = cache_size, path = ?key.path);
129 }
130 Some((returned_key, _)) => {
131 if returned_key == key {
132 debug!(cache_event = "update", cache_size = cache_size, path = ?key.path);
133 } else {
134 debug!(cache_event = "eviction", cache_size = cache_size, path = ?key.path, evicted_path = ?returned_key.path);
135 }
136 }
137 }
138 });
139 }
140
141 #[instrument(skip(self))]
143 pub fn get_directory(&self, key: &DirectoryCacheKey) -> Option<Arc<AnalysisOutput>> {
144 lock_or_recover(&self.directory_cache, DIR_CACHE_CAPACITY, |guard| {
145 let result = guard.get(key).cloned();
146 let cache_size = guard.len();
147 if let Some(v) = result {
148 debug!(cache_event = "hit", cache_size = cache_size);
149 Some(v)
150 } else {
151 debug!(cache_event = "miss", cache_size = cache_size);
152 None
153 }
154 })
155 }
156
157 #[instrument(skip(self, value))]
159 pub fn put_directory(&self, key: DirectoryCacheKey, value: Arc<AnalysisOutput>) {
160 lock_or_recover(&self.directory_cache, DIR_CACHE_CAPACITY, |guard| {
161 let push_result = guard.push(key, value);
162 let cache_size = guard.len();
163 match push_result {
164 None => {
165 debug!(cache_event = "insert", cache_size = cache_size);
166 }
167 Some((_, _)) => {
168 debug!(cache_event = "eviction", cache_size = cache_size);
169 }
170 }
171 });
172 }
173}
174
175impl Clone for AnalysisCache {
176 fn clone(&self) -> Self {
177 Self {
178 file_capacity: self.file_capacity,
179 cache: Arc::clone(&self.cache),
180 directory_cache: Arc::clone(&self.directory_cache),
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_from_entries_skips_dirs() {
191 let dir = tempfile::tempdir().expect("tempdir");
193 let file = tempfile::NamedTempFile::new_in(dir.path()).expect("tempfile");
194 let file_path = file.path().to_path_buf();
195
196 let entries = vec![
197 WalkEntry {
198 path: dir.path().to_path_buf(),
199 depth: 0,
200 is_dir: true,
201 is_symlink: false,
202 symlink_target: None,
203 },
204 WalkEntry {
205 path: file_path.clone(),
206 depth: 0,
207 is_dir: false,
208 is_symlink: false,
209 symlink_target: None,
210 },
211 ];
212
213 let key = DirectoryCacheKey::from_entries(&entries, None, AnalysisMode::Overview);
215
216 assert_eq!(key.files.len(), 1);
219 assert_eq!(key.files[0].0, file_path);
220 }
221}