use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::core::ParsedFile;
use parking_lot::RwLock;
use super::persistent_cache::PersistentCache;
use super::sieve_cache::SieveCache;
const DEFAULT_L1_CAPACITY: usize = 1000;
#[derive(Debug, Clone)]
struct CacheEntry {
parsed: ParsedFile,
modified_at: SystemTime,
size_bytes: u64,
}
pub struct AnalysisCache {
entries: RwLock<SieveCache<PathBuf, CacheEntry>>,
capacity: usize,
}
impl AnalysisCache {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_L1_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: RwLock::new(SieveCache::new(capacity)),
capacity,
}
}
pub fn get(&self, path: &Path) -> Option<ParsedFile> {
let mut entries = self.entries.write();
let entry = entries.get(&path.to_path_buf())?.clone();
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
let size = metadata.len();
if modified == entry.modified_at && size == entry.size_bytes {
return Some(entry.parsed.clone());
}
}
}
None
}
pub fn put(&self, path: &Path, parsed: ParsedFile) {
if let Ok(metadata) = std::fs::metadata(path) {
let modified_at = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let size_bytes = metadata.len();
let entry = CacheEntry {
parsed,
modified_at,
size_bytes,
};
self.entries.write().insert(path.to_path_buf(), entry);
}
}
pub fn invalidate(&self, path: &Path) {
self.entries.write().remove(&path.to_path_buf());
}
pub fn clear(&self) {
self.entries.write().clear();
}
pub fn len(&self) -> usize {
self.entries.read().len()
}
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
pub fn evict_stale(&self) {
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn hit_ratio(&self) -> f64 {
self.entries.read().hit_ratio()
}
}
impl Default for AnalysisCache {
fn default() -> Self {
Self::new()
}
}
pub struct TwoLevelCache {
l1: RwLock<SieveCache<PathBuf, CacheEntry>>,
l2: RwLock<PersistentCache>,
}
impl TwoLevelCache {
pub fn new(l1_capacity: usize, l2: PersistentCache) -> Self {
Self {
l1: RwLock::new(SieveCache::new(l1_capacity)),
l2: RwLock::new(l2),
}
}
pub fn get(&self, path: &Path) -> Option<ParsedFile> {
let path_buf = path.to_path_buf();
{
let mut l1 = self.l1.write();
if let Some(entry) = l1.get(&path_buf) {
let entry = entry.clone();
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
let size = metadata.len();
if modified == entry.modified_at && size == entry.size_bytes {
return Some(entry.parsed);
}
}
}
}
}
let path_str = path.to_string_lossy().to_string();
let l2 = self.l2.read();
if let Some(cached) = l2.get_entry(&path_str) {
let source = std::fs::read_to_string(path).ok()?;
let metadata = std::fs::metadata(path).ok()?;
let modified_at = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let size_bytes = metadata.len();
let current_hash = crate::analysis::persistent_cache::hash_content(source.as_bytes());
if current_hash != cached.content_hash {
return None;
}
let language = crate::core::Language::from_extension(
path.extension().and_then(|e| e.to_str()).unwrap_or(""),
)
.unwrap_or(crate::core::Language::Python);
let mut parsed = ParsedFile::new(path_str, language, source);
parsed.nodes = cached.nodes.clone();
parsed.edges = cached.edges.clone();
parsed.entry_points = cached.entry_points.clone();
let entry = CacheEntry {
parsed: parsed.clone(),
modified_at,
size_bytes,
};
self.l1.write().insert(path_buf, entry);
return Some(parsed);
}
None
}
pub fn put(&self, path: &Path, parsed: ParsedFile) {
if let Ok(metadata) = std::fs::metadata(path) {
let modified_at = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let size_bytes = metadata.len();
let entry = CacheEntry {
parsed,
modified_at,
size_bytes,
};
self.l1.write().insert(path.to_path_buf(), entry);
}
}
pub fn invalidate(&self, path: &Path) {
self.l1.write().remove(&path.to_path_buf());
let path_str = path.to_string_lossy().to_string();
self.l2.write().remove_entry(&path_str);
}
pub fn clear_l1(&self) {
self.l1.write().clear();
}
pub fn l1_len(&self) -> usize {
self.l1.read().len()
}
pub fn l2_len(&self) -> usize {
self.l2.read().len()
}
pub fn l1_hit_ratio(&self) -> f64 {
self.l1.read().hit_ratio()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::persistent_cache::{hash_content, CachedFileEntry, PersistentCache};
use crate::core::Language;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_cache_hit_and_miss() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
fs::write(&file_path, "def test(): pass").unwrap();
let cache = AnalysisCache::new();
assert!(cache.get(&file_path).is_none());
let parsed = ParsedFile::new(
file_path.to_string_lossy().to_string(),
Language::Python,
"def test(): pass".to_string(),
);
cache.put(&file_path, parsed);
assert!(cache.get(&file_path).is_some());
assert_eq!(cache.len(), 1);
}
#[test]
fn test_cache_invalidation() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
fs::write(&file_path, "def test(): pass").unwrap();
let cache = AnalysisCache::new();
let parsed = ParsedFile::new(
file_path.to_string_lossy().to_string(),
Language::Python,
"def test(): pass".to_string(),
);
cache.put(&file_path, parsed);
assert_eq!(cache.len(), 1);
cache.invalidate(&file_path);
assert_eq!(cache.len(), 0);
}
#[test]
fn test_cache_evict_stale() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
fs::write(&file_path, "def test(): pass").unwrap();
let cache = AnalysisCache::new();
let parsed = ParsedFile::new(
file_path.to_string_lossy().to_string(),
Language::Python,
"def test(): pass".to_string(),
);
cache.put(&file_path, parsed);
fs::remove_file(&file_path).unwrap();
cache.invalidate(&file_path);
assert_eq!(cache.len(), 0);
}
#[test]
fn test_cache_with_capacity() {
let cache = AnalysisCache::with_capacity(50);
assert_eq!(cache.capacity(), 50);
assert!(cache.is_empty());
}
#[test]
fn test_sieve_eviction_under_capacity() {
let dir = TempDir::new().unwrap();
let cache = AnalysisCache::with_capacity(2);
let file_a = dir.path().join("a.py");
let file_b = dir.path().join("b.py");
let file_c = dir.path().join("c.py");
fs::write(&file_a, "def a(): pass").unwrap();
fs::write(&file_b, "def b(): pass").unwrap();
fs::write(&file_c, "def c(): pass").unwrap();
let mk = |path: &Path| {
ParsedFile::new(
path.to_string_lossy().to_string(),
Language::Python,
"pass".to_string(),
)
};
cache.put(&file_a, mk(&file_a));
cache.put(&file_b, mk(&file_b));
assert_eq!(cache.len(), 2);
cache.put(&file_c, mk(&file_c));
assert_eq!(cache.len(), 2);
}
#[test]
fn test_two_level_cache_l1_hit() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
fs::write(&file_path, "def test(): pass").unwrap();
let l2 = PersistentCache::new();
let cache = TwoLevelCache::new(100, l2);
assert!(cache.get(&file_path).is_none());
let parsed = ParsedFile::new(
file_path.to_string_lossy().to_string(),
Language::Python,
"def test(): pass".to_string(),
);
cache.put(&file_path, parsed);
assert!(cache.get(&file_path).is_some());
assert_eq!(cache.l1_len(), 1);
}
#[test]
fn test_two_level_cache_l2_promotion() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
let content = "def test(): pass";
fs::write(&file_path, content).unwrap();
let mut l2 = PersistentCache::new();
let content_hash = hash_content(content.as_bytes());
let path_str = file_path.to_string_lossy().to_string();
l2.update_entry(CachedFileEntry {
path: path_str,
content_hash,
nodes: vec![],
edges: vec![],
entry_points: vec![],
});
let cache = TwoLevelCache::new(100, l2);
assert_eq!(cache.l1_len(), 0);
let result = cache.get(&file_path);
assert!(result.is_some());
assert_eq!(cache.l1_len(), 1);
}
#[test]
fn test_two_level_cache_invalidate() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.py");
let content = "def test(): pass";
fs::write(&file_path, content).unwrap();
let mut l2 = PersistentCache::new();
let content_hash = hash_content(content.as_bytes());
let path_str = file_path.to_string_lossy().to_string();
l2.update_entry(CachedFileEntry {
path: path_str,
content_hash,
nodes: vec![],
edges: vec![],
entry_points: vec![],
});
let cache = TwoLevelCache::new(100, l2);
cache.get(&file_path);
assert_eq!(cache.l1_len(), 1);
assert_eq!(cache.l2_len(), 1);
cache.invalidate(&file_path);
assert_eq!(cache.l1_len(), 0);
assert_eq!(cache.l2_len(), 0);
}
}