use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use lru::LruCache;
use crate::extract::FileMapL1;
use crate::path::RelPath;
const STREAM_CHUNK: usize = 256;
pub(crate) struct FileMeta {
pub(crate) hash_hex: Box<str>,
pub(crate) language: Box<str>,
pub(crate) size_bytes: u64,
}
#[derive(Default)]
pub(crate) struct FileIndexView {
files: BTreeMap<RelPath, FileMeta>,
}
impl FileIndexView {
pub(crate) fn build(store: &crate::store::Store) -> Self {
use rayon::prelude::*;
let files: BTreeMap<RelPath, FileMeta> = store
.index
.files
.par_iter()
.filter_map(|(path, entry)| {
store
.blob_path_fm_hex(&entry.hash_hex)
.is_file()
.then(|| (path.clone(), FileMeta::from(entry)))
})
.collect();
Self { files }
}
pub(crate) fn len(&self) -> usize {
self.files.len()
}
pub(crate) fn get(&self, path: &RelPath) -> Option<&FileMeta> {
self.files.get(path)
}
#[cfg(any(test, feature = "documents", feature = "memory"))]
pub(crate) fn contains(&self, path: &RelPath) -> bool {
self.files.contains_key(path)
}
pub(crate) fn keys(&self) -> impl Iterator<Item = &RelPath> {
self.files.keys()
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (&RelPath, &FileMeta)> {
self.files.iter()
}
#[cfg(test)]
pub(crate) fn from_pairs(pairs: impl IntoIterator<Item = (RelPath, FileMeta)>) -> Self {
Self {
files: pairs.into_iter().collect(),
}
}
pub(crate) fn with_delta(&self, store: &crate::store::Store, updated: &[RelPath], removed: &[RelPath]) -> Self {
let mut files: BTreeMap<RelPath, FileMeta> = self
.files
.iter()
.map(|(path, meta)| (path.clone(), meta.clone()))
.collect();
for path in removed {
files.remove(path);
}
for path in updated {
match store.index.files.get(path) {
Some(entry) if store.blob_path_fm_hex(&entry.hash_hex).is_file() => {
files.insert(path.clone(), FileMeta::from(entry));
}
_ => {
files.remove(path);
}
}
}
Self { files }
}
}
impl From<&crate::store::FileEntry> for FileMeta {
fn from(entry: &crate::store::FileEntry) -> Self {
Self {
hash_hex: entry.hash_hex.as_str().into(),
language: entry.language.as_str().into(),
size_bytes: entry.size_bytes,
}
}
}
impl Clone for FileMeta {
fn clone(&self) -> Self {
Self {
hash_hex: self.hash_hex.clone(),
language: self.language.clone(),
size_bytes: self.size_bytes,
}
}
}
struct Charged {
lru: LruCache<Box<str>, Arc<FileMapL1>>,
charged: u64,
}
pub(crate) struct L1Cache {
blobs_dir: PathBuf,
budget_bytes: u64,
inner: Mutex<Charged>,
misses: AtomicU64,
}
impl L1Cache {
pub(crate) fn new(blobs_dir: PathBuf, budget_bytes: u64) -> Self {
Self {
blobs_dir,
budget_bytes,
inner: Mutex::new(Charged {
lru: LruCache::unbounded(),
charged: 0,
}),
misses: AtomicU64::new(0),
}
}
pub(crate) fn placeholder() -> Self {
Self::new(PathBuf::new(), 0)
}
pub(crate) fn blobs_dir(&self) -> &std::path::Path {
&self.blobs_dir
}
pub(crate) fn budget_bytes(&self) -> u64 {
self.budget_bytes
}
#[cfg(test)]
pub(crate) fn charged_bytes(&self) -> u64 {
self.lock().charged
}
#[cfg(test)]
pub(crate) fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
pub(crate) fn load(&self, hash_hex: &str) -> Option<Arc<FileMapL1>> {
if let Some(hit) = self.lock().lru.get(hash_hex).cloned() {
return Some(hit);
}
let decoded = Arc::new(self.read_blob(hash_hex)?);
self.admit(hash_hex, &decoded);
Some(decoded)
}
fn load_chunk(&self, hashes: &[&str]) -> Vec<Option<Arc<FileMapL1>>> {
use rayon::prelude::*;
let mut slots: Vec<Option<Arc<FileMapL1>>> = vec![None; hashes.len()];
let mut misses: Vec<usize> = Vec::new();
{
let mut guard = self.lock();
for (i, hash) in hashes.iter().enumerate() {
match guard.lru.get(*hash) {
Some(hit) => slots[i] = Some(Arc::clone(hit)),
None => misses.push(i),
}
}
}
if misses.is_empty() {
return slots;
}
let decoded: Vec<(usize, Option<Arc<FileMapL1>>)> = misses
.par_iter()
.map(|&i| (i, self.read_blob(hashes[i]).map(Arc::new)))
.collect();
for (i, value) in decoded {
if let Some(value) = value {
self.admit(hashes[i], &value);
slots[i] = Some(value);
}
}
slots
}
fn read_blob(&self, hash_hex: &str) -> Option<FileMapL1> {
self.misses.fetch_add(1, Ordering::Relaxed);
crate::store_blob::read_l1_blob_in(&self.blobs_dir, hash_hex)
.ok()
.flatten()
}
fn admit(&self, hash_hex: &str, value: &Arc<FileMapL1>) {
let bytes = l1_heap_bytes(value);
let mut guard = self.lock();
if let Some(previous) = guard.lru.put(hash_hex.into(), Arc::clone(value)) {
guard.charged = guard.charged.saturating_sub(l1_heap_bytes(&previous));
}
guard.charged = guard.charged.saturating_add(bytes);
if self.budget_bytes == 0 {
return;
}
while guard.charged > self.budget_bytes && guard.lru.len() > 1 {
let Some((_, evicted)) = guard.lru.pop_lru() else {
break;
};
guard.charged = guard.charged.saturating_sub(l1_heap_bytes(&evicted));
}
}
#[cfg(test)]
pub(crate) fn seed(&self, hash_hex: &str, value: Arc<FileMapL1>) {
self.admit(hash_hex, &value);
}
fn lock(&self) -> std::sync::MutexGuard<'_, Charged> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
}
pub(crate) fn stream_while<F>(view: &FileIndexView, cache: &L1Cache, mut f: F)
where
F: FnMut(&RelPath, &FileMapL1) -> bool,
{
let mut paths: Vec<&RelPath> = Vec::with_capacity(STREAM_CHUNK);
let mut hashes: Vec<&str> = Vec::with_capacity(STREAM_CHUNK);
for (path, meta) in view.iter() {
paths.push(path);
hashes.push(&meta.hash_hex);
if paths.len() == STREAM_CHUNK && !drain(cache, &mut paths, &mut hashes, &mut f) {
return;
}
}
drain(cache, &mut paths, &mut hashes, &mut f);
}
fn drain<F>(cache: &L1Cache, paths: &mut Vec<&RelPath>, hashes: &mut Vec<&str>, f: &mut F) -> bool
where
F: FnMut(&RelPath, &FileMapL1) -> bool,
{
let loaded = cache.load_chunk(hashes);
let mut keep_going = true;
for (path, value) in paths.iter().zip(loaded) {
if let Some(value) = value
&& !f(path, &value)
{
keep_going = false;
break;
}
}
paths.clear();
hashes.clear();
keep_going
}
pub(crate) fn l1_heap_bytes(l1: &FileMapL1) -> u64 {
let mut total = std::mem::size_of::<FileMapL1>() as u64;
total += l1.language.len() as u64;
total += (l1.symbols.len() * std::mem::size_of::<crate::extract::Symbol>()) as u64;
for sym in &l1.symbols {
total += sym.name.len() as u64;
total += sym.signature.as_ref().map_or(0, String::len) as u64;
total += (sym.decorators.len() * std::mem::size_of::<String>()) as u64;
total += sym.decorators.iter().map(|d| d.len() as u64).sum::<u64>();
}
total += (l1.imports.len() * std::mem::size_of::<crate::extract::Import>()) as u64;
for imp in &l1.imports {
total += imp.module.as_ref().map_or(0, String::len) as u64;
total += imp.raw.len() as u64;
}
total += (l1.implementations.len() * std::mem::size_of::<crate::extract::Implementation>()) as u64;
for imp in &l1.implementations {
total += (imp.trait_name.len() + imp.impl_type.len()) as u64;
}
total += (l1.rationale.len() * std::mem::size_of::<crate::extract::RationaleRecord>()) as u64;
for rec in &l1.rationale {
total += rec.text.len() as u64;
total += (rec.citations.len() * std::mem::size_of::<String>()) as u64;
total += rec.citations.iter().map(|c| c.len() as u64).sum::<u64>();
}
total
}
pub(crate) fn budget_bytes_from(resources: &crate::config::ResourcesConfig) -> u64 {
(resources.max_map_cache_mb as u64).saturating_mul(1024 * 1024)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::{Symbol, SymbolKind};
fn l1_with(name: &str, padding: usize) -> FileMapL1 {
FileMapL1 {
schema_ver: crate::extract::SCHEMA_VER,
language: "rust".to_string(),
size_bytes: 1,
had_errors: false,
error_count: 0,
symbols: vec![Symbol {
name: name.to_string(),
kind: SymbolKind::Function,
start_byte: 0,
end_byte: 1,
start_row: 0,
start_col: 0,
signature: Some("x".repeat(padding)),
decorators: Vec::new(),
}],
imports: Vec::new(),
implementations: Vec::new(),
rationale: Vec::new(),
extract_epoch: crate::extract::EXTRACT_EPOCH,
}
}
#[test]
fn heap_bytes_tracks_payload_not_entry_count() {
let small = l1_heap_bytes(&l1_with("a", 1));
let large = l1_heap_bytes(&l1_with("a", 100_000));
assert!(large > small + 99_000, "small={small} large={large}");
}
#[test]
fn admit_evicts_to_budget_but_keeps_the_newest() {
let cache = L1Cache::new(PathBuf::new(), 4096);
for i in 0..64 {
let value = Arc::new(l1_with(&format!("sym{i}"), 512));
cache.admit(&format!("{i:064x}"), &value);
}
assert!(cache.charged_bytes() <= 4096, "charged={}", cache.charged_bytes());
let huge = Arc::new(l1_with("huge", 1_000_000));
cache.admit("ff", &huge);
assert!(cache.load("ff").is_some(), "the newest entry is never evicted");
}
#[test]
fn zero_budget_never_evicts() {
let cache = L1Cache::new(PathBuf::new(), 0);
for i in 0..32 {
let value = Arc::new(l1_with(&format!("sym{i}"), 4096));
cache.admit(&format!("{i:064x}"), &value);
}
assert!(cache.charged_bytes() > 32 * 4096);
}
}