use super::*;
#[test]
fn non_cache_bucket_equals_rss_minus_cache_and_lease() {
let target = 4 * 1024 * 1024 * 1024_u64; let rss_value = 2 * 1024 * 1024 * 1024_u64; let rss = FakeRss::new(rss_value);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 512 * 1024 * 1024);
a.set_current_bytes(200 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let _lease = budget.lease(300 * 1024 * 1024);
let snap = budget.tick_now();
assert_eq!(snap.cache_bytes_current(), 200 * 1024 * 1024);
assert_eq!(snap.lease_bytes_active, 300 * 1024 * 1024);
assert_eq!(snap.non_cache_bytes(), 1548 * 1024 * 1024);
}
struct FixedJemalloc(JemallocStats);
impl JemallocStatsSource for FixedJemalloc {
fn read(&self) -> JemallocStats {
self.0
}
}
#[test]
fn non_cache_live_bytes_subtracts_cache_and_lease_from_allocated() {
let target = 4 * 1024 * 1024 * 1024_u64;
let rss = FakeRss::new(2 * 1024 * 1024 * 1024_u64);
let budget = budget_with_target(target, Arc::clone(&rss)).with_jemalloc_stats(Box::new(
FixedJemalloc(JemallocStats {
allocated_bytes: 1024 * 1024 * 1024,
active_bytes: 1100 * 1024 * 1024,
metadata_bytes: 64 * 1024 * 1024,
resident_bytes: 1900 * 1024 * 1024,
mapped_bytes: 2200 * 1024 * 1024,
retained_bytes: 900 * 1024 * 1024,
}),
));
let a = FakeCache::new("a", 512 * 1024 * 1024);
a.set_current_bytes(200 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let _lease = budget.lease(300 * 1024 * 1024);
let snap = budget.tick_now();
assert_eq!(snap.rss_bytes, 2048 * 1024 * 1024);
assert_eq!(snap.rss_sysinfo_bytes, 2 * 1024 * 1024 * 1024);
assert_eq!(snap.non_cache_bytes(), 1548 * 1024 * 1024);
assert_eq!(snap.non_cache_live_bytes(), 524 * 1024 * 1024);
assert_eq!(snap.jemalloc.retained_bytes, 900 * 1024 * 1024);
}
#[test]
fn reporter_attributes_a_slice_of_non_cache_live() {
let target = 16 * 1024 * 1024 * 1024_u64;
let rss = FakeRss::new(8 * 1024 * 1024 * 1024_u64);
let budget = budget_with_target(target, Arc::clone(&rss)).with_jemalloc_stats(Box::new(
FixedJemalloc(JemallocStats {
allocated_bytes: 10 * 1024 * 1024 * 1024,
active_bytes: 10 * 1024 * 1024 * 1024,
metadata_bytes: 0,
resident_bytes: 10 * 1024 * 1024 * 1024,
mapped_bytes: 10 * 1024 * 1024 * 1024,
retained_bytes: 0,
}),
));
let reporter = Arc::new(AtomicReporter::new("reasoner"));
let gauge = reporter.gauge();
let dyn_reporter: Arc<dyn NonCacheReporter> = reporter.clone();
budget.register_reporter(&dyn_reporter);
gauge.set(6 * 1024 * 1024 * 1024);
let snap = budget.tick_now();
assert_eq!(snap.reporters.len(), 1);
assert_eq!(snap.reporter_bytes(), 6 * 1024 * 1024 * 1024);
assert_eq!(snap.non_cache_live_bytes(), 10 * 1024 * 1024 * 1024);
assert_eq!(snap.non_cache_other_bytes(), 4 * 1024 * 1024 * 1024);
}
#[test]
fn dropped_reporter_prunes_from_next_tick() {
let target = 4 * 1024 * 1024 * 1024_u64;
let rss = FakeRss::new(target);
let budget = budget_with_target(target, Arc::clone(&rss));
let reporter = Arc::new(AtomicReporter::new("reasoner"));
let dyn_reporter: Arc<dyn NonCacheReporter> = reporter.clone();
budget.register_reporter(&dyn_reporter);
assert_eq!(budget.tick_now().reporters.len(), 1);
drop(reporter);
drop(dyn_reporter);
assert_eq!(budget.tick_now().reporters.len(), 0);
}
#[test]
fn rss_falls_back_to_sysinfo_without_jemalloc() {
let target = 1024 * 1024 * 1024;
let rss = FakeRss::new(512 * 1024 * 1024);
let budget = budget_with_target(target, Arc::clone(&rss));
let snap = budget.tick_now();
assert_eq!(snap.rss_bytes, 512 * 1024 * 1024);
assert_eq!(snap.rss_sysinfo_bytes, 512 * 1024 * 1024);
}
#[test]
fn non_cache_live_bytes_zero_without_jemalloc_source() {
let target = 1024 * 1024 * 1024;
let rss = FakeRss::new(512 * 1024 * 1024);
let budget = budget_with_target(target, Arc::clone(&rss));
let snap = budget.tick_now();
assert_eq!(snap.jemalloc.allocated_bytes, 0);
assert_eq!(snap.non_cache_live_bytes(), 0);
assert_eq!(snap.non_cache_bytes(), 512 * 1024 * 1024);
}
#[test]
fn non_cache_bucket_saturates_when_caches_exceed_rss() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(50 * 1024 * 1024);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 80 * 1024 * 1024);
a.set_current_bytes(80 * 1024 * 1024); let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let snap = budget.tick_now();
assert_eq!(
snap.non_cache_bytes(),
0,
"non_cache_bytes must saturate, got {}",
snap.non_cache_bytes()
);
}
#[test]
fn verdict_streak_increments_and_resets_on_label_change() {
use crate::policy::Verdict;
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(target); let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 128 * 1024 * 1024);
a.set_current_bytes(50 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let s1 = budget.tick_now();
assert!(matches!(s1.verdict, Verdict::Hold));
assert_eq!(s1.verdict_streak, 1);
assert!(s1.previous_verdict_label.is_none());
let s2 = budget.tick_now();
assert!(matches!(s2.verdict, Verdict::Hold));
assert_eq!(s2.verdict_streak, 2);
assert!(s2.previous_verdict_label.is_none());
rss.rss.store(2 * target, Ordering::Relaxed);
a.set_current_bytes(150 * 1024 * 1024);
let s3 = budget.tick_now();
assert!(
matches!(s3.verdict, Verdict::Shrink { .. }),
"expected Shrink, got {:?}",
s3.verdict
);
assert_eq!(s3.verdict_streak, 1, "streak resets on label change");
assert_eq!(s3.previous_verdict_label, Some("hold"));
assert_eq!(s3.previous_verdict_streak, Some(2));
let s4 = budget.tick_now();
assert!(matches!(s4.verdict, Verdict::Shrink { .. }));
assert_eq!(s4.verdict_streak, 2);
assert!(s4.previous_verdict_label.is_none());
assert!(s4.previous_verdict_streak.is_none());
}
#[test]
fn cache_snapshot_delta_bytes_signed() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(2 * target); let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 64 * 1024 * 1024);
a.set_current_bytes(64 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let snap = budget.tick_now();
let cache = snap
.caches
.iter()
.find(|c| c.name == "a")
.expect("cache snapshot present");
let delta = cache.delta_bytes();
assert!(delta < 0, "expected negative delta on shrink, got {delta}");
assert_eq!(delta, -(32 * 1024 * 1024));
rss.rss.store(target, Ordering::Relaxed);
a.set_current_bytes(32 * 1024 * 1024);
let snap = budget.tick_now();
let cache = snap.caches.iter().find(|c| c.name == "a").unwrap();
assert_eq!(cache.delta_bytes(), 0, "hold should produce zero delta");
}
#[test]
fn registration_survives_weak_drop() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(target);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 16 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
drop(a);
drop(dyn_a);
let snap = budget.tick_now();
assert!(snap.caches.is_empty(), "dead caches should be pruned");
}