use crate::constants::capacity;
use crate::models::{
CodeAnalysis, CodeAnalysisApplyDiffDetail, CodeAnalysisReadDetail, CodeAnalysisRecord,
CodeAnalysisRunCommandDetail, CodeAnalysisWriteDetail, ExtensionType,
};
use crate::session::ParseMode;
use anyhow::Result;
use lru::LruCache;
use std::fs;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
#[derive(Debug, Clone)]
struct CachedFile {
fingerprint: FileFingerprint,
analysis: Arc<CodeAnalysis>,
size_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FileStamp {
modified: SystemTime,
len: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct GrokDependencyStamps {
summary: Option<FileStamp>,
updates: Option<FileStamp>,
cwd: Option<FileStamp>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FileFingerprint {
primary: FileStamp,
grok_dependencies: Option<GrokDependencyStamps>,
}
pub struct FileParseCache {
cache: RwLock<LruCache<PathBuf, CachedFile>>,
}
impl FileParseCache {
pub fn new() -> Self {
let cache_size = NonZeroUsize::new(capacity::FILE_CACHE_SIZE).unwrap();
Self {
cache: RwLock::new(LruCache::new(cache_size)),
}
}
pub fn get_or_parse<P: AsRef<Path>>(&self, path: P) -> Result<Arc<CodeAnalysis>> {
self.get_or_parse_inner(path.as_ref(), None)
}
pub fn get_or_parse_as<P: AsRef<Path>>(
&self,
path: P,
provider: ExtensionType,
) -> Result<Arc<CodeAnalysis>> {
self.get_or_parse_inner(path.as_ref(), Some(provider))
}
fn get_or_parse_inner(
&self,
path: &Path,
provider: Option<ExtensionType>,
) -> Result<Arc<CodeAnalysis>> {
let path_buf = path.to_path_buf();
let primary = file_stamp(path)?;
{
if let Ok(cache_read) = self.cache.read() {
if let Some(cached) = cache_read.peek(&path_buf) {
let is_grok = provider == Some(ExtensionType::Grok)
|| cached.analysis.extension_name == "Grok";
let fingerprint = FileFingerprint {
primary,
grok_dependencies: is_grok.then(|| grok_dependency_stamps(path)),
};
if cached.fingerprint == fingerprint {
log::trace!("LRU cache hit for {}", path.display());
let result = Arc::clone(&cached.analysis);
drop(cache_read);
if let Ok(mut cache_write) = self.cache.write() {
cache_write.get(&path_buf); }
return Ok(result);
}
}
}
}
log::debug!("LRU cache miss for {}, parsing...", path.display());
let possible_grok_dependencies = (provider.is_none()
|| provider == Some(ExtensionType::Grok))
.then(|| grok_dependency_stamps(path));
let analysis = match provider {
Some(p) => crate::session::parse_session_file_typed_as(path, p, ParseMode::Full)?,
None => crate::session::parse_session_file_typed(path)?,
};
let arc_analysis = Arc::new(analysis);
let size_bytes = estimate_analysis_bytes(arc_analysis.as_ref());
if let Ok(mut cache_write) = self.cache.write() {
let is_grok =
provider == Some(ExtensionType::Grok) || arc_analysis.extension_name == "Grok";
cache_write.put(
path_buf,
CachedFile {
fingerprint: FileFingerprint {
primary,
grok_dependencies: is_grok.then_some(possible_grok_dependencies).flatten(),
},
analysis: Arc::clone(&arc_analysis),
size_bytes,
},
);
}
Ok(arc_analysis)
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.write() {
cache.clear();
}
}
pub fn cleanup_stale(&self) {
if let Ok(mut cache) = self.cache.write() {
let stale_keys: Vec<PathBuf> = cache
.iter()
.filter(|(path, _)| !path.exists())
.map(|(path, _)| path.clone())
.collect();
for key in stale_keys {
cache.pop(&key);
}
}
}
pub fn stats(&self) -> CacheStats {
if let Ok(cache) = self.cache.write() {
let total_bytes: usize = cache.iter().map(|(_, c)| c.size_bytes).sum();
CacheStats {
entry_count: cache.len(),
estimated_memory_kb: total_bytes / 1024,
}
} else {
CacheStats::default()
}
}
pub fn invalidate<P: AsRef<Path>>(&self, path: P) {
if let Ok(mut cache) = self.cache.write() {
cache.pop(&path.as_ref().to_path_buf());
}
}
pub fn get_cached_paths(&self) -> Vec<PathBuf> {
if let Ok(cache) = self.cache.write() {
cache.iter().map(|(path, _)| path.clone()).collect()
} else {
Vec::new()
}
}
}
fn file_stamp(path: &Path) -> Result<FileStamp> {
let metadata = fs::metadata(path)?;
Ok(FileStamp {
modified: metadata.modified()?,
len: metadata.len(),
})
}
fn optional_file_stamp(path: &Path) -> Option<FileStamp> {
let metadata = fs::metadata(path).ok()?;
Some(FileStamp {
modified: metadata.modified().ok()?,
len: metadata.len(),
})
}
fn grok_dependency_stamps(signals_path: &Path) -> GrokDependencyStamps {
GrokDependencyStamps {
summary: optional_file_stamp(&signals_path.with_file_name("summary.json")),
updates: optional_file_stamp(&signals_path.with_file_name("updates.jsonl")),
cwd: signals_path
.parent()
.and_then(Path::parent)
.and_then(|workspace| optional_file_stamp(&workspace.join(".cwd"))),
}
}
impl Default for FileParseCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Clone)]
pub struct CacheStats {
pub entry_count: usize,
pub estimated_memory_kb: usize,
}
fn estimate_analysis_bytes(analysis: &CodeAnalysis) -> usize {
use std::mem::size_of;
let mut bytes = size_of::<CodeAnalysis>();
bytes += analysis.user.capacity();
bytes += analysis.extension_name.capacity();
bytes += analysis.insights_version.capacity();
bytes += analysis.machine_id.capacity();
bytes += analysis.records.capacity() * size_of::<CodeAnalysisRecord>();
for record in &analysis.records {
bytes += record.task_id.capacity();
bytes += record.folder_path.capacity();
bytes += record.git_remote_url.capacity();
bytes += record.write_file_details.capacity() * size_of::<CodeAnalysisWriteDetail>();
for detail in &record.write_file_details {
bytes += detail.base.file_path.capacity();
bytes += detail.content.capacity();
}
bytes += record.read_file_details.capacity() * size_of::<CodeAnalysisReadDetail>();
for detail in &record.read_file_details {
bytes += detail.base.file_path.capacity();
}
bytes += record.edit_file_details.capacity() * size_of::<CodeAnalysisApplyDiffDetail>();
for detail in &record.edit_file_details {
bytes += detail.base.file_path.capacity();
bytes += detail.old_string.capacity();
bytes += detail.new_string.capacity();
}
bytes += record.run_command_details.capacity() * size_of::<CodeAnalysisRunCommandDetail>();
for detail in &record.run_command_details {
bytes += detail.base.file_path.capacity();
bytes += detail.command.capacity();
bytes += detail.description.capacity();
}
for (k, _) in &record.conversation_usage {
bytes += k.capacity();
bytes += 256;
}
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_basic() {
let cache = FileParseCache::new();
let stats = cache.stats();
assert_eq!(stats.entry_count, 0);
}
#[test]
fn test_cache_clear() {
let cache = FileParseCache::new();
cache.clear();
let stats = cache.stats();
assert_eq!(stats.entry_count, 0);
}
}