use md5::{Digest, Md5};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReuseOutcome {
Cold,
DiskContentHit,
RenderCacheHit,
UnchangedStub,
CrossFileRef,
FreshBypass,
Stale,
VariantEvicted,
PolicyBypass,
}
impl ReuseOutcome {
const fn is_render_reuse(self) -> bool {
matches!(
self,
Self::RenderCacheHit | Self::UnchangedStub | Self::CrossFileRef
)
}
const fn is_ctx_read_eligible(self) -> bool {
!matches!(self, Self::FreshBypass | Self::PolicyBypass)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ReuseSnapshot {
pub cold: u64,
pub disk_content_hits: u64,
pub render_cache_hits: u64,
pub unchanged_stubs: u64,
pub cross_file_refs: u64,
pub fresh_bypasses: u64,
pub stale: u64,
pub variant_evicted: u64,
pub policy_bypasses: u64,
pub eligible_ctx_reads: u64,
pub reused_renderings: u64,
pub eligible_search_reads: u64,
}
impl ReuseSnapshot {
pub fn read_reuse_rate(&self) -> f64 {
rate(self.reused_renderings, self.eligible_ctx_reads)
}
pub fn disk_reuse_rate(&self) -> f64 {
rate(self.disk_content_hits, self.eligible_search_reads)
}
}
struct ReuseCounters {
cold: AtomicU64,
disk_content_hits: AtomicU64,
render_cache_hits: AtomicU64,
unchanged_stubs: AtomicU64,
cross_file_refs: AtomicU64,
fresh_bypasses: AtomicU64,
stale: AtomicU64,
variant_evicted: AtomicU64,
policy_bypasses: AtomicU64,
eligible_ctx_reads: AtomicU64,
reused_renderings: AtomicU64,
eligible_search_reads: AtomicU64,
}
impl ReuseCounters {
const fn new() -> Self {
Self {
cold: AtomicU64::new(0),
disk_content_hits: AtomicU64::new(0),
render_cache_hits: AtomicU64::new(0),
unchanged_stubs: AtomicU64::new(0),
cross_file_refs: AtomicU64::new(0),
fresh_bypasses: AtomicU64::new(0),
stale: AtomicU64::new(0),
variant_evicted: AtomicU64::new(0),
policy_bypasses: AtomicU64::new(0),
eligible_ctx_reads: AtomicU64::new(0),
reused_renderings: AtomicU64::new(0),
eligible_search_reads: AtomicU64::new(0),
}
}
}
static REUSE_COUNTERS: ReuseCounters = ReuseCounters::new();
fn rate(numerator: u64, denominator: u64) -> f64 {
if denominator == 0 {
0.0
} else {
numerator as f64 * 100.0 / denominator as f64
}
}
fn bump_outcome(counters: &ReuseCounters, outcome: ReuseOutcome) {
let counter = match outcome {
ReuseOutcome::Cold => &counters.cold,
ReuseOutcome::DiskContentHit => &counters.disk_content_hits,
ReuseOutcome::RenderCacheHit => &counters.render_cache_hits,
ReuseOutcome::UnchangedStub => &counters.unchanged_stubs,
ReuseOutcome::CrossFileRef => &counters.cross_file_refs,
ReuseOutcome::FreshBypass => &counters.fresh_bypasses,
ReuseOutcome::Stale => &counters.stale,
ReuseOutcome::VariantEvicted => &counters.variant_evicted,
ReuseOutcome::PolicyBypass => &counters.policy_bypasses,
};
counter.fetch_add(1, Ordering::Relaxed);
}
fn record_ctx_read_outcome_into(counters: &ReuseCounters, outcome: ReuseOutcome) {
bump_outcome(counters, outcome);
if outcome.is_ctx_read_eligible() {
counters.eligible_ctx_reads.fetch_add(1, Ordering::Relaxed);
if outcome.is_render_reuse() {
counters.reused_renderings.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn record_ctx_read_outcome(outcome: ReuseOutcome) {
record_ctx_read_outcome_into(&REUSE_COUNTERS, outcome);
}
fn record_search_content_read_into(counters: &ReuseCounters, hit: bool) {
counters
.eligible_search_reads
.fetch_add(1, Ordering::Relaxed);
bump_outcome(
counters,
if hit {
ReuseOutcome::DiskContentHit
} else {
ReuseOutcome::Cold
},
);
}
pub fn record_search_content_read(hit: bool) {
record_search_content_read_into(&REUSE_COUNTERS, hit);
}
#[must_use]
pub fn reuse_snapshot() -> ReuseSnapshot {
snapshot_from(&REUSE_COUNTERS)
}
fn snapshot_from(counters: &ReuseCounters) -> ReuseSnapshot {
ReuseSnapshot {
cold: counters.cold.load(Ordering::Relaxed),
disk_content_hits: counters.disk_content_hits.load(Ordering::Relaxed),
render_cache_hits: counters.render_cache_hits.load(Ordering::Relaxed),
unchanged_stubs: counters.unchanged_stubs.load(Ordering::Relaxed),
cross_file_refs: counters.cross_file_refs.load(Ordering::Relaxed),
fresh_bypasses: counters.fresh_bypasses.load(Ordering::Relaxed),
stale: counters.stale.load(Ordering::Relaxed),
variant_evicted: counters.variant_evicted.load(Ordering::Relaxed),
policy_bypasses: counters.policy_bypasses.load(Ordering::Relaxed),
eligible_ctx_reads: counters.eligible_ctx_reads.load(Ordering::Relaxed),
reused_renderings: counters.reused_renderings.load(Ordering::Relaxed),
eligible_search_reads: counters.eligible_search_reads.load(Ordering::Relaxed),
}
}
pub fn file_mtime(path: &str) -> Option<SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}
pub fn is_cache_entry_stale(path: &str, cached_mtime: Option<SystemTime>) -> bool {
let current = file_mtime(path);
match (cached_mtime, current) {
(None, None) => false,
(Some(_), None) | (None, Some(_)) => true,
(Some(cached), Some(current)) => current != cached,
}
}
const VERIFY_HASH_CAP_BYTES: u64 = 8 * 1024 * 1024;
fn cache_verify_enabled() -> bool {
std::env::var("LEAN_CTX_CACHE_VERIFY").map_or(true, |v| v != "0")
}
pub fn is_cache_entry_stale_verified(
path: &str,
cached_mtime: Option<SystemTime>,
cached_hash: &str,
) -> bool {
let mtime_stale = is_cache_entry_stale(path, cached_mtime);
if !mtime_stale && (cached_hash.is_empty() || !cache_verify_enabled()) {
return false;
}
if cached_hash.is_empty() {
return mtime_stale;
}
let Ok(meta) = std::fs::metadata(path) else {
return true;
};
if meta.len() > VERIFY_HASH_CAP_BYTES {
return mtime_stale;
}
match std::fs::read(path) {
Ok(bytes) => compute_md5(&String::from_utf8_lossy(&bytes)) != cached_hash,
Err(_) => true,
}
}
pub(super) fn compute_md5(content: &str) -> String {
let mut hasher = Md5::new();
hasher.update(content.as_bytes());
crate::core::agent_identity::hex_encode(&hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::{
ReuseCounters, ReuseOutcome, ReuseSnapshot, record_ctx_read_outcome_into,
record_search_content_read_into, snapshot_from,
};
#[test]
fn every_read_records_one_exclusive_terminal_outcome() {
let counters = ReuseCounters::new();
record_ctx_read_outcome_into(&counters, ReuseOutcome::RenderCacheHit);
record_ctx_read_outcome_into(&counters, ReuseOutcome::UnchangedStub);
record_ctx_read_outcome_into(&counters, ReuseOutcome::FreshBypass);
record_ctx_read_outcome_into(&counters, ReuseOutcome::PolicyBypass);
record_search_content_read_into(&counters, true);
record_search_content_read_into(&counters, false);
let snapshot = snapshot_from(&counters);
assert_eq!(snapshot.render_cache_hits, 1);
assert_eq!(snapshot.unchanged_stubs, 1);
assert_eq!(snapshot.fresh_bypasses, 1);
assert_eq!(snapshot.policy_bypasses, 1);
assert_eq!(snapshot.disk_content_hits, 1);
assert_eq!(snapshot.cold, 1);
assert_eq!(snapshot.eligible_ctx_reads, 2);
assert_eq!(snapshot.reused_renderings, 2);
assert_eq!(snapshot.eligible_search_reads, 2);
}
#[test]
fn reuse_rates_keep_read_and_disk_denominators_separate() {
let metrics = ReuseSnapshot {
reused_renderings: 3,
eligible_ctx_reads: 4,
disk_content_hits: 2,
eligible_search_reads: 5,
..ReuseSnapshot::default()
};
assert_eq!(metrics.read_reuse_rate(), 75.0);
assert_eq!(metrics.disk_reuse_rate(), 40.0);
}
#[test]
fn only_render_reuse_outcomes_count_for_ctx_read_reuse() {
assert!(ReuseOutcome::RenderCacheHit.is_render_reuse());
assert!(ReuseOutcome::UnchangedStub.is_render_reuse());
assert!(ReuseOutcome::CrossFileRef.is_render_reuse());
assert!(!ReuseOutcome::DiskContentHit.is_render_reuse());
assert!(!ReuseOutcome::FreshBypass.is_render_reuse());
assert!(!ReuseOutcome::Stale.is_render_reuse());
}
#[test]
fn bypasses_are_not_eligible_for_read_reuse() {
assert!(!ReuseOutcome::FreshBypass.is_ctx_read_eligible());
assert!(!ReuseOutcome::PolicyBypass.is_ctx_read_eligible());
assert!(ReuseOutcome::Stale.is_ctx_read_eligible());
}
#[test]
fn stale_mtime_but_same_hash_is_not_stale() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("poll.txt");
std::fs::write(&p, "terminal output line 1").unwrap();
let mtime1 = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5("terminal output line 1");
std::thread::sleep(std::time::Duration::from_millis(50));
{
let mut f = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&p)
.unwrap();
f.write_all(b"terminal output line 1").unwrap();
}
let mtime2 = std::fs::metadata(&p).unwrap().modified().unwrap();
assert_ne!(mtime1, mtime2, "mtime must differ after rewrite");
let stale = super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime1), &hash);
assert!(
!stale,
"same content with different mtime must NOT be stale"
);
}
#[test]
fn stale_mtime_with_changed_content_is_stale() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("changed.txt");
std::fs::write(&p, "version 1").unwrap();
let mtime1 = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5("version 1");
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&p, "version 2 with new content").unwrap();
let stale = super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime1), &hash);
assert!(stale, "changed content must be stale");
}
#[test]
fn same_mtime_same_hash_is_not_stale() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("stable.txt");
std::fs::write(&p, "stable content").unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5("stable content");
let stale = super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash);
assert!(!stale, "unchanged file must not be stale");
}
#[test]
fn empty_hash_falls_back_to_mtime_only() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("nohash.txt");
std::fs::write(&p, "content").unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
assert!(
!super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), ""),
"same mtime + empty hash = not stale"
);
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&p, "content").unwrap();
assert!(
super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), ""),
"different mtime + empty hash = stale (no hash to rescue)"
);
}
#[test]
fn large_file_uses_mtime_only() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("big.txt");
let big = "x".repeat(9 * 1024 * 1024);
std::fs::write(&p, &big).unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5(&big);
assert!(
!super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"same mtime on large file = not stale"
);
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&p, &big).unwrap();
assert!(
super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"large files skip hash check even if content matches"
);
}
#[test]
fn deleted_file_is_stale() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("ephemeral.txt");
std::fs::write(&p, "temp").unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5("temp");
std::fs::remove_file(&p).unwrap();
assert!(
super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"deleted file must be stale"
);
}
#[test]
fn empty_file_hash_matches_correctly() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("empty.txt");
std::fs::write(&p, "").unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5("");
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&p, "").unwrap();
assert!(
!super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"empty file rewritten with same content must not be stale"
);
}
#[test]
fn rapid_touch_cycles_stay_cached() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("rapid.txt");
let content = "rapid polling content that stays stable";
std::fs::write(&p, content).unwrap();
let mtime_initial = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5(content);
for _ in 0..10 {
std::thread::sleep(std::time::Duration::from_millis(10));
std::fs::write(&p, content).unwrap();
let stale = super::is_cache_entry_stale_verified(
p.to_str().unwrap(),
Some(mtime_initial),
&hash,
);
assert!(
!stale,
"repeated rewrites of same content must never be stale"
);
}
}
#[test]
fn binary_content_hash_works() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("binary.bin");
let data: Vec<u8> = (0..=255).collect();
std::fs::write(&p, &data).unwrap();
let mtime = std::fs::metadata(&p).unwrap().modified().unwrap();
let hash = super::compute_md5(&String::from_utf8_lossy(&data));
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&p, &data).unwrap();
assert!(
!super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"binary content with same bytes must not be stale"
);
let mut changed = data.clone();
changed[100] = 0;
std::fs::write(&p, &changed).unwrap();
assert!(
super::is_cache_entry_stale_verified(p.to_str().unwrap(), Some(mtime), &hash),
"binary content with 1 byte changed must be stale"
);
}
}