diff --git a/src/app/view/stats.rs b/src/app/view/stats.rs index e9248ae..3d4fd3b 100644 --- a/src/app/view/stats.rs +++ b/src/app/view/stats.rs @@ -371,21 +371,25 @@ impl App { .into() } - /// Disc usage of both cache buckets, split by age. The bar is the age - /// distribution of what is stored; the budget governs the transient store - /// only and is reported on its own row. + /// Disc usage of both cache buckets. The bar splits the transient store by + /// age — the order the budget evicts in — and carries the session + /// checkpoints as one further segment, since those are only freed with + /// their session and so compete for disc without ever ageing out. fn cache_explorer(&self) -> Element<'_, Message> { let usage = &self.kv_cache_report; let total = usage.total_bytes(); let capacity = total.max(1); + let segments = crate::metrics::CACHE_AGE_BUCKETS + .iter() + .enumerate() + .map(|(index, (label, _))| (*label, usage.age_bytes[index], AGE_COLORS[index])) + .chain([("Sessions", usage.session_bytes, SESSION_COLOR)]); let mut bar = row![].height(14).spacing(2); let mut legend = column![].spacing(6); - for (index, (label, _)) in crate::metrics::CACHE_AGE_BUCKETS.iter().enumerate() { - let bytes = usage.age_bytes[index]; + for (label, bytes, color) in segments { if bytes == 0 { continue; } - let color = AGE_COLORS[index]; bar = bar.push( container(Space::new(Length::Fill, Length::Fill)) .width(Length::FillPortion(portion(bytes, capacity))) @@ -394,7 +398,7 @@ impl App { legend = legend.push( row![ container(Space::new(9, 9)).style(move |_| chart_bar_style(color)), - text(*label).size(12).color(muted_text()), + text(label).size(12).color(muted_text()), Space::with_width(Length::Fill), text(format!( "{} · {:.0}%", @@ -497,6 +501,9 @@ impl App { } } +/// Sessions sit outside the age scale: they are not evicted, only deleted. +const SESSION_COLOR: Color = Color::from_rgb(0.85, 0.44, 0.56); + /// Age buckets from green (fresh) to grey (past every hit half-life). const AGE_COLORS: [Color; 5] = [ Color::from_rgb(0.28, 0.69, 0.44), diff --git a/src/metrics.rs b/src/metrics.rs index 7fe9555..f84376b 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -707,7 +707,8 @@ pub(crate) struct KvCacheReport { pub(crate) session_files: u64, pub(crate) transient_bytes: u64, pub(crate) transient_files: u64, - /// Bytes per [`CACHE_AGE_BUCKETS`] entry, both buckets combined. + /// Bytes per [`CACHE_AGE_BUCKETS`] entry, transient store only: session + /// checkpoints never age out, so their age says nothing about eviction. pub(crate) age_bytes: [u64; CACHE_AGE_BUCKETS.len()], pub(crate) entries: Vec, } @@ -752,11 +753,13 @@ pub(crate) fn kv_cache_report(root: &Path, budget_bytes: u64) -> KvCacheReport { .ok() .and_then(|modified| now.duration_since(modified).ok()) .map_or(0, |age| age.as_secs()); - let bucket = CACHE_AGE_BUCKETS - .iter() - .position(|(_, limit)| age_seconds < *limit) - .unwrap_or(CACHE_AGE_BUCKETS.len() - 1); - report.age_bytes[bucket] += bytes; + if transient { + let bucket = CACHE_AGE_BUCKETS + .iter() + .position(|(_, limit)| age_seconds < *limit) + .unwrap_or(CACHE_AGE_BUCKETS.len() - 1); + report.age_bytes[bucket] += bytes; + } // Index files are counted in the totals but folded into their // checkpoint's row. if path.extension().is_some_and(|value| value == "meta") { @@ -915,8 +918,9 @@ mod tests { assert_eq!(report.session_files, 1); assert_eq!(report.transient_bytes, 140); assert_eq!(report.transient_files, 2); - // Fresh files in the first bucket, the two-day-old one in "1–7d". - assert_eq!(report.age_bytes[0], 340); + // Only the transient store is bucketed by age: its fresh index file in + // the first bucket, its two-day-old checkpoint in "1–7d". + assert_eq!(report.age_bytes[0], 40); assert_eq!(report.age_bytes[3], 100); assert_eq!( (