diff --git a/crates/tracedecay-runtime-core/src/resident_memory.rs b/crates/tracedecay-runtime-core/src/resident_memory.rs index af90cb3e4e..c1cbe49b82 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory.rs @@ -726,6 +726,15 @@ impl ResidentMemoryPressureV1 { self.state() } + /// Publish the canonical sampler's reading after a reclaimer released memory, + /// without the checkpoint throttle and without reentering reclaimers. + fn publish_post_reclaim_sample(&self) { + if let Some(sample) = (self.sampler)() { + self.publish_observation(sample.admission_bytes()); + self.publish_over_budget_gauge(); + } + } + fn checkpoint_micros(&self) -> u64 { u64::try_from(self.checkpoint_epoch.elapsed().as_micros()).unwrap_or(u64::MAX - 1) } @@ -1056,7 +1065,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.publish_post_reclaim_sample(); } 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..2e05825c68 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory/tests.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory/tests.rs @@ -1566,3 +1566,53 @@ fn allocator_reclaim_preserves_cgroup_pressure_until_sample_recovers() { .is_ok() ); } + +#[test] +fn allocator_reclaim_publishes_recovery_after_recent_checkpoint() { + let limit = bytes(1024 * 1024 * 1024 * 1024); + let sample = Arc::new(Mutex::new(ProcessResidentSampleV1 { + resident_bytes: 1, + unreclaimable_bytes: 1, + swapped_bytes: 0, + cgroup_committed_bytes: Some(limit.get()), + })); + let sampled = Arc::clone(&sample); + let pressure = Arc::new(ResidentMemoryPressureV1::with_sampler( + limit, + Arc::new(move || Some(*sampled.lock().expect("sample"))), + )); + let _trim = super::register_process_allocator_pressure_reclaimer_v1(&pressure) + .expect("register allocator reclaimer"); + let authority = Arc::new(ProcessResidentMemoryV1::with_pressure( + limit, + Arc::clone(&pressure), + )); + let shed = Arc::clone(&sample); + let _owner_shed = pressure + .register_pressure_reclaimer( + super::RESIDENT_OWNERS_PRESSURE_PRIORITY_V1, + Arc::new(move |_| { + shed.lock().expect("sample").cgroup_committed_bytes = Some(1); + 0 + }), + ) + .expect("register owner reclaimer"); + + assert!(matches!( + pressure.sample_for_checkpoint(), + ResidentMemoryPressureStateV1::OverBudget { .. } + )); + pressure.publish_observed_resident_bytes(limit.get()); + assert!(matches!( + pressure.state(), + ResidentMemoryPressureStateV1::Nominal { .. } + )); + assert!( + authority + .reserve( + key("project", "worktree", "generation", "graph"), + growth_request() + ) + .is_ok() + ); +}