diff --git a/crates/tracedecay-runtime-core/src/resident_memory.rs b/crates/tracedecay-runtime-core/src/resident_memory.rs index af90cb3e4e..2112c19eeb 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory.rs @@ -726,6 +726,14 @@ impl ResidentMemoryPressureV1 { self.state() } + /// Refresh pressure after reclaim without running reclaimers recursively. + fn resample_after_reclaim(&self) -> Option { + let sample = (self.sampler)()?; + self.publish_observation(sample.admission_bytes()); + self.publish_over_budget_gauge(); + Some(self.state()) + } + fn checkpoint_micros(&self) -> u64 { u64::try_from(self.checkpoint_epoch.elapsed().as_micros()).unwrap_or(u64::MAX - 1) } @@ -1056,7 +1064,7 @@ pub fn register_process_allocator_pressure_reclaimer_v1( Arc::new(move |request| { let trim = release_process_allocator_memory_v1(); if let Some(pressure) = pressure_weak.upgrade() { - pressure.sample_for_checkpoint(); + pressure.resample_after_reclaim(); } tracing::info!( event = "process_allocator_trimmed", diff --git a/crates/tracedecay-runtime-core/src/resident_memory/tests.rs b/crates/tracedecay-runtime-core/src/resident_memory/tests.rs index af033c2f1d..15614c4895 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory/tests.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory/tests.rs @@ -1163,6 +1163,46 @@ fn allocator_trim_reclaimer_runs_under_pressure_and_reports_only_measured_releas } } +#[test] +fn allocator_trim_readmits_growth_when_memory_recovers_inside_checkpoint_throttle() { + let limit = bytes(PRESSURE_TEST_LIMIT_BYTES); + let measured = Arc::new(AtomicU64::new(PRESSURE_TEST_LIMIT_BYTES)); + let sampled = Arc::clone(&measured); + let pressure = Arc::new(ResidentMemoryPressureV1::with_sampler( + limit, + Arc::new(move || { + let observed = sampled.load(Ordering::Acquire); + Some(ProcessResidentSampleV1 { + resident_bytes: observed, + unreclaimable_bytes: observed, + swapped_bytes: 0, + cgroup_committed_bytes: None, + }) + }), + )); + let authority = Arc::new(ProcessResidentMemoryV1::with_pressure( + limit, + Arc::clone(&pressure), + )); + let _trim = super::register_process_allocator_pressure_reclaimer_v1(&pressure) + .expect("allocator trim registration"); + let owner = key("project-a", "worktree-a", "generation-a", "canonical"); + + pressure.sample_for_checkpoint(); + let refused = authority + .reserve(owner.clone(), growth_request()) + .expect_err("a process at its limit refuses growth"); + assert!(refused.is_observed_over_budget()); + + measured.store(64 * 1024 * 1024, Ordering::Release); + pressure.publish_observed_resident_bytes(PRESSURE_TEST_LIMIT_BYTES); + + let _admitted = authority + .reserve(owner, growth_request()) + .expect("growth is admitted once the trim measures recovered memory"); + assert_eq!(authority.snapshot().used_bytes, 16 * 1024 * 1024); +} + #[test] fn allocator_pressure_reclaimer_installation_preserves_registration_failure() { let pressure = Arc::new(ResidentMemoryPressureV1::new(bytes(