use std::cell::RefCell;
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
#[cfg(test)]
thread_local! {
static METADATA_CALL_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub fn reset_metadata_call_count() {
METADATA_CALL_COUNT.with(|c| c.set(0));
}
#[cfg(test)]
pub fn metadata_call_count() -> usize {
METADATA_CALL_COUNT.with(std::cell::Cell::get)
}
use super::circular::CircularAnalysis;
use super::file_lookup::collect_rs_files_recursive;
use crate::metadata::StructMetadata;
mod lookups;
pub use lookups::{
get_circular_analysis, get_fk_column, get_module_path_from_schema_path,
get_struct_from_schema_path,
};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct FileFingerprint {
mtime: SystemTime,
len: u64,
}
#[derive(Clone)]
struct DirEntry {
fingerprint: u64,
last_epoch_validated: u64,
files: Arc<[PathBuf]>,
}
#[derive(Clone)]
struct PathLookupEntry<T> {
value: T,
fingerprint: u64,
last_epoch_validated: u64,
}
struct FileCache {
file_lists: HashMap<PathBuf, DirEntry>,
file_contents: HashMap<PathBuf, (FileFingerprint, Arc<String>)>,
missing_file_content_epoch: HashMap<PathBuf, u64>,
file_disk_reads: usize,
content_cache_hits: usize,
struct_parses: usize,
ast_parses: usize,
circular_analysis: HashMap<(String, String), CircularAnalysis>,
struct_lookup: HashMap<String, PathLookupEntry<Option<Arc<StructMetadata>>>>,
fk_column_lookup: HashMap<(String, String), PathLookupEntry<Option<String>>>,
module_path_cache: HashMap<String, Vec<String>>,
struct_definitions: HashMap<PathBuf, (FileFingerprint, HashMap<String, String>)>,
manifest_dir: Option<String>,
manifest_dir_epoch: u64,
circular_cache_hits: usize,
struct_lookup_cache_hits: usize,
fk_column_cache_hits: usize,
module_path_cache_hits: usize,
struct_def_cache_hits: usize,
epoch: u64,
mtime_epoch_cache: HashMap<PathBuf, (u64, Option<FileFingerprint>)>,
}
thread_local! {
static FILE_CACHE: RefCell<FileCache> = RefCell::new(FileCache {
file_lists: HashMap::with_capacity(4),
file_contents: HashMap::with_capacity(32),
missing_file_content_epoch: 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,
manifest_dir_epoch: 0,
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,
epoch: 0,
mtime_epoch_cache: HashMap::with_capacity(32),
});
}
pub fn bump_epoch() {
FILE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.epoch = cache.epoch.wrapping_add(1);
});
}
fn get_fingerprint_cached(cache: &mut FileCache, path: &Path) -> Option<FileFingerprint> {
let current_epoch = cache.epoch;
if let Some(&(entry_epoch, fingerprint)) = cache.mtime_epoch_cache.get(path)
&& entry_epoch == current_epoch
{
return fingerprint;
}
#[cfg(test)]
METADATA_CALL_COUNT.with(|c| c.set(c.get() + 1));
let fingerprint = std::fs::metadata(path).ok().and_then(|m| {
m.modified().ok().map(|mtime| FileFingerprint {
mtime,
len: m.len(),
})
});
cache
.mtime_epoch_cache
.insert(path.to_path_buf(), (current_epoch, fingerprint));
fingerprint
}
pub fn get_file_fingerprint(path: &Path) -> Option<FileFingerprint> {
FILE_CACHE.with(|cache| get_fingerprint_cached(&mut cache.borrow_mut(), path))
}
pub fn get_manifest_dir() -> Option<String> {
FILE_CACHE.with(|cache| get_manifest_dir_inner(&mut cache.borrow_mut()))
}
fn get_manifest_dir_inner(cache: &mut FileCache) -> Option<String> {
let epoch = cache.epoch;
if cache.manifest_dir_epoch == epoch
&& 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);
cache.manifest_dir_epoch = epoch;
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()
}
fn walk_and_fingerprint(cache: &mut FileCache, dir: &Path) -> (Vec<PathBuf>, u64) {
let mut files = Vec::new();
collect_rs_files_recursive(dir, &mut files);
files.sort();
let mut hasher = DefaultHasher::new();
for path in &files {
path.hash(&mut hasher);
if let Some(fp) = get_fingerprint_cached(cache, path) {
if let Ok(duration) = fp.mtime.duration_since(std::time::UNIX_EPOCH) {
duration.as_secs().hash(&mut hasher);
duration.subsec_nanos().hash(&mut hasher);
}
fp.len.hash(&mut hasher);
}
}
(files, hasher.finish())
}
fn ensure_file_list(cache: &mut FileCache, src_dir: &Path) -> Arc<[PathBuf]> {
let current_epoch = cache.epoch;
if let Some(entry) = cache.file_lists.get(src_dir)
&& entry.last_epoch_validated == current_epoch
{
return Arc::clone(&entry.files);
}
let (files_vec, fp) = walk_and_fingerprint(cache, src_dir);
if let Some(entry) = cache.file_lists.get_mut(src_dir)
&& entry.fingerprint == fp
{
entry.last_epoch_validated = current_epoch;
return Arc::clone(&entry.files);
}
let files: Arc<[PathBuf]> = files_vec.into();
cache.file_lists.insert(
src_dir.to_path_buf(),
DirEntry {
fingerprint: fp,
last_epoch_validated: current_epoch,
files: Arc::clone(&files),
},
);
files
}
fn ensure_struct_definitions(cache: &mut FileCache, path: &Path) -> bool {
let current_fp = get_fingerprint_cached(cache, path);
if let Some(fp) = current_fp
&& let Some((cached_fp, _)) = cache.struct_definitions.get(path)
&& *cached_fp == fp
{
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(fp) = current_fp {
cache
.struct_definitions
.insert(path.to_path_buf(), (fp, 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<Arc<String>> {
let current_fp = get_fingerprint_cached(cache, path);
let current_epoch = cache.epoch;
if let Some(fp) = current_fp
&& let Some((cached_fp, content)) = cache.file_contents.get(path)
&& *cached_fp == fp
{
cache.content_cache_hits += 1;
return Some(Arc::clone(content));
}
if current_fp.is_none()
&& cache
.missing_file_content_epoch
.get(path)
.is_some_and(|epoch| *epoch == current_epoch)
{
return None;
}
let Some(content) = std::fs::read_to_string(path).ok().map(Arc::new) else {
if current_fp.is_none() {
cache
.missing_file_content_epoch
.insert(path.to_path_buf(), current_epoch);
}
return None;
};
cache.file_disk_reads += 1;
if let Some(fp) = current_fp {
cache.missing_file_content_epoch.remove(path);
cache
.file_contents
.insert(path.to_path_buf(), (fp, Arc::clone(&content)));
}
Some(content)
}
pub fn parse_struct_cached(definition: &str) -> Result<syn::ItemStruct, syn::Error> {
FILE_CACHE.with(|cache| cache.borrow_mut().struct_parses += 1);
syn::parse_str(definition)
}
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",
cache.file_lists.len(),
cache.file_contents.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 fingerprint = std::fs::metadata(path).ok().map_or(
FileFingerprint {
mtime: std::time::SystemTime::UNIX_EPOCH,
len: 0,
},
|m| FileFingerprint {
mtime: m
.modified()
.ok()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH),
len: m.len(),
},
);
let entry = cache
.struct_definitions
.entry(path.to_path_buf())
.or_insert_with(|| (fingerprint, HashMap::new()));
entry.0 = fingerprint;
entry.1.insert(name.to_string(), definition.to_string());
});
}
#[cfg(test)]
pub fn fk_lookup_contains(schema_path: &str, via_rel: &str) -> bool {
FILE_CACHE.with(|cache| {
cache
.borrow()
.fk_column_lookup
.contains_key(&(schema_path.to_string(), via_rel.to_string()))
})
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod coverage_tests {
use super::*;
#[test]
fn readable_file_populates_content_and_all_struct_definition_caches() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("models.rs");
std::fs::write(
&path,
"pub struct Alpha { pub id: i32 } pub struct Beta { pub name: String }",
)
.unwrap();
bump_epoch();
let alpha = get_struct_definition(std::hint::black_box(&path), "Alpha")
.expect("Alpha must be extracted from the readable file");
let beta = get_struct_definition(&path, "Beta")
.expect("Beta must be extracted by the same full-file parse");
assert!(alpha.contains("id : i32"));
assert!(beta.contains("name : String"));
FILE_CACHE.with(|cache| {
let cache = cache.borrow();
assert_eq!(cache.struct_definitions[&path].1.len(), 2);
assert!(cache.file_contents[&path].1.contains("struct Alpha"));
});
}
}