use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use super::circular::CircularAnalysis;
use super::file_lookup::collect_rs_files_recursive;
use crate::metadata::StructMetadata;
struct FileCache {
file_lists: HashMap<PathBuf, Vec<PathBuf>>,
file_contents: HashMap<PathBuf, (SystemTime, String)>,
struct_candidates: HashMap<(PathBuf, String), Vec<PathBuf>>,
file_disk_reads: usize,
content_cache_hits: usize,
struct_parses: usize,
ast_parses: usize,
circular_analysis: HashMap<(String, String), CircularAnalysis>,
struct_lookup: HashMap<String, Option<StructMetadata>>,
fk_column_lookup: HashMap<(String, String), Option<String>>,
module_path_cache: HashMap<String, Vec<String>>,
struct_definitions: HashMap<PathBuf, (SystemTime, HashMap<String, String>)>,
manifest_dir: Option<String>,
circular_cache_hits: usize,
struct_lookup_cache_hits: usize,
fk_column_cache_hits: usize,
module_path_cache_hits: usize,
struct_def_cache_hits: usize,
}
thread_local! {
static FILE_CACHE: RefCell<FileCache> = RefCell::new(FileCache {
file_lists: HashMap::with_capacity(4),
file_contents: HashMap::with_capacity(32),
struct_candidates: HashMap::with_capacity(32),
file_disk_reads: 0,
content_cache_hits: 0,
struct_parses: 0,
ast_parses: 0,
circular_analysis: HashMap::with_capacity(16),
struct_lookup: HashMap::with_capacity(32),
fk_column_lookup: HashMap::with_capacity(16),
module_path_cache: HashMap::with_capacity(32),
manifest_dir: None,
circular_cache_hits: 0,
struct_lookup_cache_hits: 0,
fk_column_cache_hits: 0,
module_path_cache_hits: 0,
struct_definitions: HashMap::with_capacity(32),
struct_def_cache_hits: 0,
});
}
pub fn get_manifest_dir() -> Option<String> {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(ref dir) = cache.manifest_dir {
return Some(dir.clone());
}
let dir = std::env::var("CARGO_MANIFEST_DIR").ok();
cache.manifest_dir.clone_from(&dir);
dir
})
}
pub fn get_parsed_file(path: &Path) -> Option<syn::File> {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
parse_file_cached(&mut cache, path)
})
}
fn parse_file_cached(cache: &mut FileCache, path: &Path) -> Option<syn::File> {
let content = get_file_content_inner(cache, path)?;
cache.ast_parses += 1;
syn::parse_file(&content).ok()
}
pub fn get_struct_candidates(src_dir: &Path, struct_name: &str) -> Vec<PathBuf> {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let key = (src_dir.to_path_buf(), struct_name.to_string());
if let Some(candidates) = cache.struct_candidates.get(&key) {
return candidates.clone();
}
let files = if let Some(files) = cache.file_lists.get(src_dir) {
files.clone()
} else {
let mut files = Vec::new();
collect_rs_files_recursive(src_dir, &mut files);
cache
.file_lists
.insert(src_dir.to_path_buf(), files.clone());
files
};
let candidates: Vec<PathBuf> = files
.into_iter()
.filter(|path| {
let content = get_file_content_inner(&mut cache, path);
content.is_some_and(|c| c.contains(struct_name))
})
.collect();
cache.struct_candidates.insert(key, candidates.clone());
candidates
})
}
fn ensure_struct_definitions(cache: &mut FileCache, path: &Path) -> bool {
let current_mtime = std::fs::metadata(path).ok().and_then(|m| m.modified().ok());
if let Some(mtime) = current_mtime
&& let Some((cached_mtime, _)) = cache.struct_definitions.get(path)
&& *cached_mtime == mtime
{
cache.struct_def_cache_hits += 1;
return true;
}
let Some(file_ast) = parse_file_cached(cache, path) else {
return false;
};
let mut defs = HashMap::new();
for item in &file_ast.items {
if let syn::Item::Struct(struct_item) = item {
let name = struct_item.ident.to_string();
let def = quote::quote!(#struct_item).to_string();
defs.insert(name, def);
}
}
if let Some(mtime) = current_mtime {
cache
.struct_definitions
.insert(path.to_path_buf(), (mtime, defs));
}
true
}
pub fn get_struct_definition(path: &Path, struct_name: &str) -> Option<String> {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if !ensure_struct_definitions(&mut cache, path) {
return None;
}
cache
.struct_definitions
.get(path)?
.1
.get(struct_name)
.cloned()
})
}
fn get_file_content_inner(cache: &mut FileCache, path: &Path) -> Option<String> {
let current_mtime = std::fs::metadata(path).ok().and_then(|m| m.modified().ok());
if let Some(mtime) = current_mtime
&& let Some((cached_mtime, content)) = cache.file_contents.get(path)
&& *cached_mtime == mtime
{
cache.content_cache_hits += 1;
return Some(content.clone());
}
let content = std::fs::read_to_string(path).ok()?;
cache.file_disk_reads += 1;
if let Some(mtime) = current_mtime {
cache
.file_contents
.insert(path.to_path_buf(), (mtime, content.clone()));
}
Some(content)
}
pub fn parse_struct_cached(definition: &str) -> Result<syn::ItemStruct, syn::Error> {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.struct_parses += 1;
syn::parse_str(definition)
})
}
pub fn get_circular_analysis(source_module_path: &[String], definition: &str) -> CircularAnalysis {
let key = (source_module_path.join("::"), definition.to_string());
let cached = FILE_CACHE.with(|cache| cache.borrow().circular_analysis.get(&key).cloned());
if let Some(result) = cached {
FILE_CACHE.with(|cache| cache.borrow_mut().circular_cache_hits += 1);
return result;
}
let result = super::circular::analyze_circular_refs(source_module_path, definition);
FILE_CACHE.with(|cache| {
cache
.borrow_mut()
.circular_analysis
.insert(key, result.clone());
});
result
}
pub fn get_struct_from_schema_path(path_str: &str) -> Option<StructMetadata> {
let cached = FILE_CACHE.with(|cache| cache.borrow().struct_lookup.get(path_str).cloned());
if let Some(result) = cached {
FILE_CACHE.with(|cache| cache.borrow_mut().struct_lookup_cache_hits += 1);
return result;
}
let result = super::file_lookup::find_struct_from_schema_path(path_str);
FILE_CACHE.with(|cache| {
cache
.borrow_mut()
.struct_lookup
.insert(path_str.to_string(), result.clone());
});
result
}
pub fn get_fk_column(schema_path: &str, via_rel: &str) -> Option<String> {
let key = (schema_path.to_string(), via_rel.to_string());
let cached = FILE_CACHE.with(|cache| cache.borrow().fk_column_lookup.get(&key).cloned());
if let Some(result) = cached {
FILE_CACHE.with(|cache| cache.borrow_mut().fk_column_cache_hits += 1);
return result;
}
let result = super::file_lookup::find_fk_column_from_target_entity(schema_path, via_rel);
FILE_CACHE.with(|cache| {
cache
.borrow_mut()
.fk_column_lookup
.insert(key, result.clone());
});
result
}
pub fn get_module_path_from_schema_path(schema_path: &proc_macro2::TokenStream) -> Vec<String> {
let path_str = schema_path.to_string();
let cached = FILE_CACHE.with(|cache| cache.borrow().module_path_cache.get(&path_str).cloned());
if let Some(result) = cached {
FILE_CACHE.with(|cache| cache.borrow_mut().module_path_cache_hits += 1);
return result;
}
let segments: Vec<&str> = path_str
.split("::")
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let result = if segments.len() > 1 {
segments[..segments.len() - 1]
.iter()
.map(ToString::to_string)
.collect()
} else {
vec![]
};
FILE_CACHE.with(|cache| {
cache
.borrow_mut()
.module_path_cache
.insert(path_str, result.clone());
});
result
}
pub fn print_profile_summary() {
if std::env::var("VESPERA_PROFILE").is_err() {
return;
}
FILE_CACHE.with(|cache| {
let cache = cache.borrow();
eprintln!("[vespera-profile] File cache stats:");
eprintln!(
" file I/O: {} disk reads, {} cache hits",
cache.file_disk_reads, cache.content_cache_hits
);
eprintln!(" struct parses: {}", cache.struct_parses);
eprintln!(" AST parses: {}", cache.ast_parses);
eprintln!(
" cache entries: {} file lists, {} file contents, {} struct candidates",
cache.file_lists.len(),
cache.file_contents.len(),
cache.struct_candidates.len()
);
eprintln!(
" circular analysis: {} cache hits, {} entries",
cache.circular_cache_hits,
cache.circular_analysis.len()
);
eprintln!(
" struct lookup: {} cache hits, {} entries",
cache.struct_lookup_cache_hits,
cache.struct_lookup.len()
);
eprintln!(
" FK column lookup: {} cache hits, {} entries",
cache.fk_column_cache_hits,
cache.fk_column_lookup.len()
);
eprintln!(
" struct definitions: {} cache hits, {} entries",
cache.struct_def_cache_hits,
cache.struct_definitions.len()
);
eprintln!(
" module path: {} cache hits, {} entries",
cache.module_path_cache_hits,
cache.module_path_cache.len()
);
});
}
#[cfg(test)]
pub fn inject_struct_definition_for_test(path: &std::path::Path, name: &str, definition: &str) {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let mtime = std::fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let entry = cache
.struct_definitions
.entry(path.to_path_buf())
.or_insert_with(|| (mtime, HashMap::new()));
entry.0 = mtime;
entry.1.insert(name.to_string(), definition.to_string());
});
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_get_struct_candidates_filters_correctly() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("has_model.rs"),
"pub struct Model { pub id: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("no_model.rs"),
"pub struct Other { pub x: i32 }",
)
.unwrap();
let candidates = get_struct_candidates(src_dir, "Model");
assert_eq!(candidates.len(), 1);
assert!(candidates[0].ends_with("has_model.rs"));
}
#[test]
fn test_get_struct_candidates_caches_result() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(src_dir.join("file.rs"), "pub struct Target { pub id: i32 }").unwrap();
let c1 = get_struct_candidates(src_dir, "Target");
let c2 = get_struct_candidates(src_dir, "Target");
assert_eq!(c1, c2, "Cached candidates should be identical");
}
#[test]
fn test_get_struct_candidates_file_list_cache_hit() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("file_a.rs"),
"pub struct Alpha { pub id: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("file_b.rs"),
"pub struct Beta { pub name: String }",
)
.unwrap();
let result1 = get_struct_candidates(src_dir, "Alpha");
assert_eq!(result1.len(), 1);
let result2 = get_struct_candidates(src_dir, "Beta");
assert_eq!(result2.len(), 1);
}
#[test]
fn test_get_fk_column_cache_hit() {
let result1 = get_fk_column("nonexistent::path::Schema", "SomeRelation");
let result2 = get_fk_column("nonexistent::path::Schema", "SomeRelation");
assert_eq!(result1, result2);
}
#[serial_test::serial]
#[test]
fn test_print_profile_summary_with_profile_env() {
unsafe { std::env::set_var("VESPERA_PROFILE", "1") };
print_profile_summary();
unsafe { std::env::remove_var("VESPERA_PROFILE") };
}
#[serial_test::serial]
#[test]
fn test_print_profile_summary_without_profile_env() {
unsafe { std::env::remove_var("VESPERA_PROFILE") };
print_profile_summary();
}
}