path_cache.rs
1 use dashmap::DashMap; 2 use parking_lot::RwLock; 3 use std::collections::HashMap; 4 use std::path::{Path, PathBuf}; 5 use std::sync::Arc; 6 7 #[derive(Clone)] 8 pub struct PathCache { 9 resolved: Arc<DashMap<PathBuf, PathBuf>>, 10 11 fallback: Arc<RwLock<HashMap<PathBuf, PathBuf>>>, 12 13 stats: Arc<RwLock<CacheStats>>, 14 } 15 16 #[derive(Debug, Default, Clone, PartialEq, Eq)] 17 pub struct CacheStats { 18 hits: usize, 19 misses: usize, 20 errors: usize, 21 } 22 23 impl PathCache { 24 pub fn new() -> Self { 25 Self { 26 resolved: Arc::new(DashMap::new()), 27 fallback: Arc::new(RwLock::new(HashMap::new())), 28 stats: Arc::new(RwLock::new(CacheStats::default())), 29 } 30 } 31 32 pub fn canonicalize(&self, path: &Path) -> PathBuf { 33 let path_buf = path.to_path_buf(); 34 35 if let Some(cached) = self.resolved.get(&path_buf) { 36 self.stats.write().hits += 1; 37 return cached.clone(); 38 } 39 40 { 41 let fallback = self.fallback.read(); 42 if let Some(cached) = fallback.get(&path_buf) { 43 self.stats.write().hits += 1; 44 return cached.clone(); 45 } 46 } 47 48 self.stats.write().misses += 1; 49 let resolved = match path.canonicalize() { 50 Ok(c) => { 51 self.resolved.insert(path_buf.clone(), c.clone()); 52 c 53 } 54 Err(_) => { 55 self.stats.write().errors += 1; 56 57 self.fallback 58 .write() 59 .insert(path_buf.clone(), path_buf.clone()); 60 path_buf 61 } 62 }; 63 64 resolved 65 } 66 67 pub fn canonicalize_many(&self, paths: &[PathBuf]) -> Vec<PathBuf> { 68 paths.iter().map(|p| self.canonicalize(p)).collect() 69 } 70 71 pub fn invalidate(&self, path: &Path) { 72 let path_buf = path.to_path_buf(); 73 self.resolved.remove(&path_buf); 74 self.fallback.write().remove(&path_buf); 75 } 76 77 pub fn clear(&self) { 78 self.resolved.clear(); 79 self.fallback.write().clear(); 80 self.stats.write().hits = 0; 81 self.stats.write().misses = 0; 82 self.stats.write().errors = 0; 83 } 84 85 pub fn stats(&self) -> CacheStats { 86 let stats = self.stats.read(); 87 CacheStats { 88 hits: stats.hits, 89 misses: stats.misses, 90 errors: stats.errors, 91 } 92 } 93 94 pub fn len(&self) -> usize { 95 self.resolved.len() + self.fallback.read().len() 96 } 97 98 pub fn is_empty(&self) -> bool { 99 self.resolved.is_empty() && self.fallback.read().is_empty() 100 } 101 102 pub fn hit_rate(&self) -> f64 { 103 let stats = self.stats.read(); 104 let total = stats.hits + stats.misses; 105 if total == 0 { 106 return 0.0; 107 } 108 stats.hits as f64 / total as f64 109 } 110 } 111 112 impl Default for PathCache { 113 fn default() -> Self { 114 Self::new() 115 } 116 } 117 118 #[cfg(test)] 119 mod tests { 120 use super::*; 121 122 #[test] 123 fn test_canonicalize_cache() { 124 let cache = PathCache::new(); 125 126 let path = PathBuf::from("."); 127 let result1 = cache.canonicalize(&path); 128 let stats = cache.stats(); 129 assert_eq!(stats.misses, 1); 130 assert_eq!(stats.hits, 0); 131 132 let result2 = cache.canonicalize(&path); 133 assert_eq!(result1, result2); 134 let stats = cache.stats(); 135 assert_eq!(stats.misses, 1); 136 assert_eq!(stats.hits, 1); 137 } 138 139 #[test] 140 fn test_hit_rate() { 141 let cache = PathCache::new(); 142 let path = PathBuf::from("."); 143 144 for _ in 0..10 { 145 cache.canonicalize(&path); 146 } 147 148 let rate = cache.hit_rate(); 149 assert!(rate >= 0.9); 150 } 151 152 #[test] 153 fn test_invalidate() { 154 let cache = PathCache::new(); 155 let path = PathBuf::from("."); 156 157 cache.canonicalize(&path); 158 assert!(!cache.is_empty()); 159 160 cache.invalidate(&path); 161 assert!(cache.is_empty() || cache.len() == 1); 162 } 163 164 #[test] 165 fn test_clear() { 166 let cache = PathCache::new(); 167 cache.canonicalize(&PathBuf::from(".")); 168 cache.canonicalize(&PathBuf::from("..")); 169 170 assert!(!cache.is_empty()); 171 cache.clear(); 172 assert!(cache.is_empty()); 173 assert_eq!(cache.stats().hits, 0); 174 } 175 176 #[test] 177 fn test_nonexistent_path() { 178 let cache = PathCache::new(); 179 let path = PathBuf::from("/nonexistent/path/that/does/not/exist"); 180 181 let result = cache.canonicalize(&path); 182 assert_eq!(result, path); 183 184 let stats = cache.stats(); 185 assert_eq!(stats.errors, 1); 186 } 187 }