//! Reclamation algorithm //! //! Implementation the of long-running [`task`] function, //! (which is the only export from here, the wider `mtracker` module). use super::*; mod deferred_drop; use deferred_drop::{DeferredDrop, GuardWithDeferredDrop}; /// Total number of participants /// /// Used in reporting and in calculations of various edge cases. /// On 64-bit systems, bigger than the refcounts, which are all `u32` type NumParticips = usize; //========== candidate victim analysis ========== /// The nominal data age of a participant #[derive(Ord, PartialOrd, Eq, PartialEq)] enum Age { /// Treat this participant as having very old data TreatAsVeryOld, /// Data age value from the [`IsParticipant`] Actual(CoarseInstant), } /// Participant status, as a candidate victim enum PStatus { /// Treat participant as having data of age OldestData Candidate(Age), /// Tear this participant down right away TearDown, /// Treat participant as not having any data; don't reclaim from it NoData, } /// Outcome of a completed reclamation run /// /// This is used only within `choose_victim`, and only for logging #[derive(Debug, derive_more::Display)] enum Outcome { /// We reached the low water mark #[display("complete")] TargetReached, /// We didn't, but we have so many participants that that's possibly expected /// /// (Can only happen on 32-bit platforms.) #[display("{} participants, good enough - stopping", n_particips)] GoodEnough { /// The number of participants n_particips: NumParticips, }, } /// Figure out whether a participant is a candidate victim, and obtain its data age fn analyse_particip(precord: &PRecord, defer_drop: &mut DeferredDrop) -> PStatus { let Some(particip) = precord.particip.upgrade() else { // Oh! This participant has vanished! // We can't reclaim from it. It may already be reclaiming. // Delete it from our data structure. return PStatus::TearDown; }; let got_oldest = catch_unwind(AssertUnwindSafe(|| particip.get_oldest(precord.enabled))); defer_drop.push(particip); match got_oldest { Ok(Some(age)) => return PStatus::Candidate(Age::Actual(age)), Ok(None) => {} Err(_panicked) => { // _panicked is of a useless type error!("bug in memory tracker: call to get_oldest panicked!"); return PStatus::TearDown; } } // The participant claims not to have any memory // There might be some cached, let's check let Some(max_cached) = precord .refcount .as_usize() .checked_mul(MAX_CACHE.as_usize()) else { // WTF! So many Participation clones that the max usage has // overflowed. (This can only happen on 32-bit platforms // since refcount is a u32.) Probably we should reclaim // from this participant. log_ratelim!( "memtrack: participant with many clones claims to have no data"; Err::(internal!("{} Participation clones", *precord.refcount)); ); return PStatus::Candidate(Age::TreatAsVeryOld); }; if precord.used.as_raw() > Qty(max_cached) { // This participant is lying to us somehow. log_ratelim!( "memtrack: participant claims to have no data, but our accounting disagrees"; Err::(internal!("{} used (by {} clones)", precord.used, *precord.refcount)); ); return PStatus::Candidate(Age::TreatAsVeryOld); } // Participant plausibly does have no data PStatus::NoData } //========== reclamation algorithm, the main pieces ========== /// State while reclamation is active struct Reclaiming { /// The heap of candidates, oldest at top of heap heap: BinaryHeap>, /// Make this type uninhabited if memory tracking is compiled out enabled: EnabledToken, } /// A victim we have selected for reclamation /// /// This designates a specific Participant. /// /// But, note that we always reclaim from an Account, so if we are reclaiming /// from one `Victim`, we may be reclaiming from other `Victim`s with the same /// `AId` and different `IsParticipant`s. And because of inheritance, we might /// be reclaiming from other Accounts too. type Victim = (AId, drop_reentrancy::ProtectedArc); /// Marker indicating that the victim's reclaim function panicked struct VictimPanicked; /// Set of responses from the victims, after they have all finished reclaiming. type VictimResponses = Vec<(AId, Result)>; impl Reclaiming { /// Check to see if we should start reclaiming, and if so return a `Reclaiming` /// /// 1. Checks to see if usage is above `max`; if not, returns `None` /// 2. Logs that we're starting reclamation /// 3. Calculates the heap of data ages fn maybe_start(state: &mut GuardWithDeferredDrop) -> Option { let (state, deferred_drop) = state.deref_mut_both(); if *state.total_used <= state.global.config.max { return None; } info!( "memory tracking: {} > {}, reclamation started (target {})", *state.total_used, state.config.max, state.config.low_water, ); // `BinaryHeap` is a max heap, so use Rev let mut heap = BinaryHeap::new(); // Build heap of participants we might want to reclaim from // (and, while we're at it, tear down broken participants) for (aid, arecord) in state.accounts.iter_mut() { arecord.ps.retain(|_pid, precord| { match analyse_particip(precord, deferred_drop) { PStatus::Candidate(age) => { heap.push(Reverse((age, aid))); true // retain } PStatus::NoData => { true // retain } PStatus::TearDown => { precord.auto_release(&mut state.global); false // remove } } }); } Some(Reclaiming { heap, enabled: state.enabled, }) } /// If we're reclaiming, choose the next victim(s) to reclaim /// /// This is the account whose participant has the oldest data age, /// and all of that account's children. /// /// We might discover that we didn't want to continue reclamation after all: /// this function is responsible for checking our progress /// against the low water mark. /// /// If reclamation should stop, this function logs, and returns `None`. fn choose_victims(&mut self, state: &mut State) -> Result>, ReclaimCrashed> { let stop = |state: &mut State, outcome| { info!( "memory tracking reclamation reached: {} (target {}): {}", *state.total_used, state.config.low_water, outcome, ); Ok(None) }; if *state.total_used <= state.config.low_water { return stop(state, Outcome::TargetReached); } let Some(Reverse((_, oldest_aid))) = self.heap.pop() else { // All our remaining participants are NoData. let n_particips: usize = state .accounts .values() .map(|ar| { ar.ps .values() .map( |pr| *pr.refcount as NumParticips, // refcount is u32, so this is fine ) .sum::() }) .sum::(); if state .total_used .as_raw() .as_usize() .checked_div(n_particips) .is_some_and(|total_used| total_used < usize::from(MAX_CACHE)) { // On 32-bit, this could happen due to the cache, if we have // 2^32 / MAX_CACHE participants. return stop(state, Outcome::GoodEnough { n_particips }); } // Oh dear. return Err(internal!( "memory accounting state corrupted: used={} n_particips={} all NoData", *state.total_used, n_particips, ) .into()); }; // When we do partial reclamation, rather than just Collapsing: // // fudge next_oldest by something to do with number of loop iterations, // to avoid one-allocation-each-time ping pong between multiple caches // // (this match statement will fail to compile when we add a non-Collapsing variant) // // let next_oldest = heap.peek_lowest(); match None { None | Some(Reclaimed::Collapsing) => {} } let victim_aids = state.get_aid_and_children_recursively(oldest_aid); let victims: Vec = { let mut particips = vec![]; for aid in victim_aids { let Some(arecord) = state.accounts.get_mut(aid) else { // shouldn't happen but no need to panic continue; }; arecord.ps.retain(|_pid, precord| { let Some(particip) = precord.particip.upgrade() else { // tear this down! precord.auto_release(&mut state.global); return false; }; particips.push((aid, particip)); true }); } particips }; Ok(Some(victims)) } /// Notify the chosen victims and obtain their responses /// /// This is the async part, and is done with the state unlocked. // Doesn't actually need `self`, only `victims`, but we take it for form's sake async fn notify_victims(&mut self, victims: Vec) -> VictimResponses { let enabled = self.enabled; futures::future::join_all( // victims.into_iter().map(|(aid, particip)| async move { let particip = particip.promise_dropping_is_ok(); // We run the `.reclaim()` calls within the same task (since that's what // `join_all` does). So they all run on whatever executor thread is polling // the reclamation task. let reclaimed = AssertUnwindSafe(particip.reclaim(enabled)) .catch_unwind() .await .map_err(|_panicked| VictimPanicked); // We drop the `ProtectedArc` here, which is OK // because we don't hold the lock. Since drop isn't async, and // `join_all` doesn't spawn tasks, we drop them sequentially. (aid, reclaimed) }), ) .await } /// Process the victim's responses and update `state` accordingly // Doesn't actually need `self`, only `state`, but we take it for form's sake fn handle_victim_responses(&mut self, state: &mut State, responses: VictimResponses) { for (aid, reclaimed) in responses { match reclaimed { Ok(Reclaimed::Collapsing) | Err(VictimPanicked) => { let Some(mut arecord) = state.accounts.remove(aid) else { // Account is gone, fair enough continue; }; arecord.auto_release(&mut state.global); // Account is definitely gone now } } } } } //========== the reclamation task, in terms of the pieces ==========- /// Return value from the task, when it finishes due to the tracker being shut down struct TaskFinished; /// Reclaim memory until we reach low water, if necessary /// /// Looks to see if we're above `config.max`. /// If so, constructs a list of victims, and starts reclaiming from them, /// until we reach low water. async fn inner_loop( tracker: &Arc, _enabled: EnabledToken, ) -> Result<(), ReclaimCrashed> { let mut reclaiming; let mut victims; { let mut state_guard = GuardWithDeferredDrop::new(tracker.lock()?.enabled_or_bug()?); let Some(r) = Reclaiming::maybe_start(&mut state_guard) else { return Ok(()); }; reclaiming = r; // Duplicating this call to reclaiming.choose_victims means we don't // release the lock between `maybe_start` and `choose_victims` (here) // and between `handle_victim_responses` and `choose_victims` (bellw). // (Releasing the lock would not be a bug, but it's not desirable.) let Some(v) = reclaiming.choose_victims(&mut state_guard)? else { return Ok(()); }; victims = v; } loop { let responses = reclaiming.notify_victims(mem::take(&mut victims)).await; let mut state_guard = tracker.lock()?.enabled_or_bug()?; reclaiming.handle_victim_responses(&mut state_guard, responses); let Some(v) = reclaiming.choose_victims(&mut state_guard)? else { return Ok(()); }; victims = v; } } /// Internal long-running task, handling reclamation - main loop /// /// Handles routine logging, but not termination async fn task_loop( tracker: &Weak, mut wakeup: mpsc::Receiver<()>, enabled: EnabledToken, ) -> Result { loop { // We don't hold a strong reference while we loop around, so we detect // last drop of an actual client handle. { let Some(tracker) = tracker.upgrade() else { return Ok(TaskFinished); }; inner_loop(&tracker, enabled).await?; } let Some(()) = wakeup.next().await else { // Sender dropped return Ok(TaskFinished); }; } } /// Internal long-running task, handling reclamation /// /// This is the entrypoint used by the rest of the `tracker`. /// It handles logging of crashes. pub(super) async fn task( tracker: Weak, wakeup: mpsc::Receiver<()>, enabled: EnabledToken, ) { match task_loop(&tracker, wakeup, enabled).await { Ok(TaskFinished) => {} Err(bug) => { let _: Option<()> = (|| { let tracker = tracker.upgrade()?; let mut state = tracker.state.as_enabled()?.lock().ok()?; state.total_used.set_poisoned(); Some(()) })(); error_report!(bug, "memory tracker task failed"); } } }