#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::DagType;
use crate::models::churn::CodeChurnAnalysis;
use crate::models::dag::DependencyGraph;
use crate::models::template::TemplateResource;
use crate::services::cache::{
config::CacheConfig,
content_cache::ContentCache,
diagnostics::{CacheDiagnostics, CacheEffectiveness, CacheStatsSnapshot},
strategies::{
AstCacheStrategy, ChurnCacheStrategy, DagCacheStrategy, GitStats, GitStatsCacheStrategy,
TemplateCacheStrategy,
},
};
use crate::services::context::FileContext;
use anyhow::Result;
use parking_lot::RwLock;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use uuid::Uuid;
pub struct SessionCacheManager {
ast_cache: Arc<RwLock<ContentCache<AstCacheStrategy>>>,
template_cache: Arc<RwLock<ContentCache<TemplateCacheStrategy>>>,
dag_cache: Arc<RwLock<ContentCache<DagCacheStrategy>>>,
churn_cache: Arc<RwLock<ContentCache<ChurnCacheStrategy>>>,
git_stats_cache: Arc<RwLock<ContentCache<GitStatsCacheStrategy>>>,
config: CacheConfig,
session_id: Uuid,
created: Instant,
}
impl SessionCacheManager {
#[must_use]
pub fn new(config: CacheConfig) -> Self {
Self {
ast_cache: Arc::new(RwLock::new(ContentCache::new(AstCacheStrategy))),
template_cache: Arc::new(RwLock::new(ContentCache::new(TemplateCacheStrategy))),
dag_cache: Arc::new(RwLock::new(ContentCache::new(DagCacheStrategy))),
churn_cache: Arc::new(RwLock::new(ContentCache::new(ChurnCacheStrategy))),
git_stats_cache: Arc::new(RwLock::new(ContentCache::new(GitStatsCacheStrategy))),
config,
session_id: Uuid::new_v4(),
created: Instant::now(),
}
}
pub async fn get_or_compute_ast<F, Fut>(
&self,
path: &Path,
compute: F,
) -> Result<Arc<FileContext>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<FileContext>>,
{
let path_buf = path.to_path_buf();
if let Some(ast) = self.ast_cache.read().get(&path_buf) {
return Ok(ast);
}
let ast = compute().await?;
self.ast_cache.write().put(path_buf, ast.clone());
Ok(Arc::new(ast))
}
pub async fn get_or_compute_template<F, Fut>(
&self,
uri: &str,
compute: F,
) -> Result<Arc<TemplateResource>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<TemplateResource>>,
{
let uri_string = uri.to_string();
if let Some(template) = self.template_cache.read().get(&uri_string) {
return Ok(template);
}
let template = compute().await?;
self.template_cache
.write()
.put(uri_string, template.clone());
Ok(Arc::new(template))
}
pub async fn get_or_compute_dag<F, Fut>(
&self,
path: &Path,
dag_type: DagType,
compute: F,
) -> Result<Arc<DependencyGraph>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<DependencyGraph>>,
{
let key = (path.to_path_buf(), dag_type);
if let Some(dag) = self.dag_cache.read().get(&key) {
return Ok(dag);
}
let dag = compute().await?;
self.dag_cache.write().put(key, dag.clone());
Ok(Arc::new(dag))
}
pub async fn get_or_compute_churn<F, Fut>(
&self,
repo: &Path,
period_days: u32,
compute: F,
) -> Result<Arc<CodeChurnAnalysis>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<CodeChurnAnalysis>>,
{
let key = (repo.to_path_buf(), period_days);
if let Some(churn) = self.churn_cache.read().get(&key) {
return Ok(churn);
}
let churn = compute().await?;
self.churn_cache.write().put(key, churn.clone());
Ok(Arc::new(churn))
}
pub async fn get_or_compute_git_stats<F, Fut>(
&self,
repo: &Path,
compute: F,
) -> Result<Arc<GitStats>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<GitStats>>,
{
let path_buf = repo.to_path_buf();
if let Some(stats) = self.git_stats_cache.read().get(&path_buf) {
return Ok(stats);
}
let stats = compute().await?;
self.git_stats_cache.write().put(path_buf, stats.clone());
Ok(Arc::new(stats))
}
#[must_use]
pub fn memory_pressure(&self) -> f32 {
let total_bytes = self.get_total_cache_size();
total_bytes as f32 / self.config.max_memory_bytes() as f32
}
#[must_use]
pub fn get_total_cache_size(&self) -> usize {
let ast_size = self.ast_cache.read().stats.memory_usage();
let template_size = self.template_cache.read().stats.memory_usage();
let dag_size = self.dag_cache.read().stats.memory_usage();
let churn_size = self.churn_cache.read().stats.memory_usage();
let git_stats_size = self.git_stats_cache.read().stats.memory_usage();
ast_size + template_size + dag_size + churn_size + git_stats_size
}
pub fn evict_if_needed(&self) {
if self.memory_pressure() > 0.8 {
for _ in 0..self.config.eviction_batch_size {
self.ast_cache.write().evict_lru();
self.template_cache.write().evict_lru();
self.dag_cache.write().evict_lru();
self.churn_cache.write().evict_lru();
self.git_stats_cache.write().evict_lru();
if self.memory_pressure() < 0.7 {
break;
}
}
}
}
pub fn clear_all(&self) {
self.ast_cache.write().clear();
self.template_cache.write().clear();
self.dag_cache.write().clear();
self.churn_cache.write().clear();
self.git_stats_cache.write().clear();
}
pub fn invalidate_file(&self, path: &Path) {
let path_str = path.to_string_lossy();
self.ast_cache
.write()
.invalidate_matching(|key| key.contains(&*path_str));
self.dag_cache
.write()
.invalidate_matching(|key| key.contains(&*path_str));
}
pub fn invalidate_directory(&self, dir: &Path) {
let dir_str = dir.to_string_lossy();
self.ast_cache
.write()
.invalidate_matching(|key| key.contains(&*dir_str));
self.dag_cache
.write()
.invalidate_matching(|key| key.contains(&*dir_str));
self.churn_cache
.write()
.invalidate_matching(|key| key.contains(&*dir_str));
}
#[must_use]
pub fn get_diagnostics(&self) -> CacheDiagnostics {
let ast_cache = self.ast_cache.read();
let template_cache = self.template_cache.read();
let dag_cache = self.dag_cache.read();
let churn_cache = self.churn_cache.read();
let git_stats_cache = self.git_stats_cache.read();
let cache_stats = vec![
(
"ast".to_string(),
CacheStatsSnapshot::from((&ast_cache.stats, ast_cache.len())),
),
(
"template".to_string(),
CacheStatsSnapshot::from((&template_cache.stats, template_cache.len())),
),
(
"dag".to_string(),
CacheStatsSnapshot::from((&dag_cache.stats, dag_cache.len())),
),
(
"churn".to_string(),
CacheStatsSnapshot::from((&churn_cache.stats, churn_cache.len())),
),
(
"git_stats".to_string(),
CacheStatsSnapshot::from((&git_stats_cache.stats, git_stats_cache.len())),
),
];
let hot_paths = ast_cache.hot_entries(10);
let effectiveness = self.calculate_effectiveness(&cache_stats);
CacheDiagnostics {
session_id: self.session_id,
uptime: self.created.elapsed(),
memory_usage_mb: self.get_total_cache_size() as f64 / (1024.0 * 1024.0),
memory_pressure: self.memory_pressure(),
cache_stats,
hot_paths,
effectiveness,
}
}
fn calculate_effectiveness(
&self,
cache_stats: &[(String, CacheStatsSnapshot)],
) -> CacheEffectiveness {
let total_hits: u64 = cache_stats.iter().map(|(_, s)| s.hits).sum();
let total_misses: u64 = cache_stats.iter().map(|(_, s)| s.misses).sum();
let total_requests = total_hits + total_misses;
let overall_hit_rate = if total_requests > 0 {
total_hits as f64 / total_requests as f64
} else {
0.0
};
let memory_efficiency = if self.config.max_memory_bytes() > 0 {
1.0 - f64::from(self.memory_pressure())
} else {
0.0
};
let time_saved_ms = total_hits * 100;
let mut valuable_caches: Vec<(String, f64)> = cache_stats
.iter()
.map(|(name, stats)| (name.clone(), stats.hits as f64))
.collect();
valuable_caches.sort_by(|a, b| b.1.total_cmp(&a.1));
valuable_caches.truncate(3);
CacheEffectiveness {
overall_hit_rate,
memory_efficiency,
time_saved_ms,
most_valuable_caches: valuable_caches,
}
}
}
unsafe impl Send for SessionCacheManager {}
unsafe impl Sync for SessionCacheManager {}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_cache_manager_creation() {
let config = CacheConfig::default();
let manager = SessionCacheManager::new(config);
assert!(manager.created.elapsed().as_secs() < 1);
}
#[test]
fn test_clear_all() {
let config = CacheConfig::default();
let manager = SessionCacheManager::new(config);
let test_path = std::path::PathBuf::from("test.rs");
manager.ast_cache.write().put(
test_path,
FileContext {
path: "test.rs".to_string(),
language: "rust".to_string(),
items: vec![],
complexity_metrics: None,
},
);
assert_eq!(manager.ast_cache.read().len(), 1);
manager.clear_all();
assert_eq!(manager.ast_cache.read().len(), 0);
}
#[test]
fn test_get_diagnostics() {
let config = CacheConfig::default();
let manager = SessionCacheManager::new(config);
let diagnostics = manager.get_diagnostics();
assert_eq!(diagnostics.session_id, manager.session_id);
assert!(diagnostics.uptime.as_secs() < 1);
assert_eq!(diagnostics.cache_stats.len(), 5); }
#[test]
fn test_memory_pressure() {
let config = CacheConfig {
max_memory_mb: 1, ..Default::default()
};
let manager = SessionCacheManager::new(config);
let diagnostics = manager.get_diagnostics();
assert_eq!(diagnostics.memory_pressure, 0.0);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}