Skip to main content

memscope_rs/capture/backends/async_tracker/
dealloc.rs

1//! Release handling for the async tracker.
2//!
3//! Kept in its own module so `async_tracker.rs` stays within the project line
4//! budget; it is an `impl` block for the same type and shares its private state.
5
6use super::*;
7
8impl AsyncTracker {
9    /// Track a deallocation associated with a task.
10    ///
11    /// Closes the *oldest* live record at the address: a late release must not
12    /// destroy a newer binding created after the address was reused. Callers that know
13    /// which binding they are releasing should use
14    /// [`Self::track_deallocation_matching`].
15    pub fn track_deallocation(&self, ptr: usize) {
16        self.close_allocation(ptr, None, None);
17    }
18
19    /// Track a deallocation for a specific address binding.
20    ///
21    /// `size` and `generation` are optional filters; providing them lets a release that
22    /// arrives late — after the address was reused — close the binding it actually
23    /// refers to instead of the newest one.
24    pub fn track_deallocation_matching(
25        &self,
26        ptr: usize,
27        size: Option<usize>,
28        generation: Option<u64>,
29    ) {
30        self.close_allocation(ptr, size, generation);
31    }
32
33    fn close_allocation(&self, ptr: usize, size: Option<usize>, generation: Option<u64>) {
34        let closed = {
35            let Ok(mut allocations) = self.allocations.lock() else {
36                tracing::error!("Failed to acquire allocations lock during track_deallocation");
37                return;
38            };
39            self.close_allocation_inner(&mut allocations, ptr, size, generation)
40        };
41
42        self.apply_deallocation_stats(closed);
43    }
44
45    /// Shared inner logic: find and remove the matching allocation record.
46    /// Returns the closed record (if any) without updating stats/profiles.
47    fn close_allocation_inner(
48        &self,
49        allocations: &mut std::sync::MutexGuard<'_, HashMap<usize, Vec<AsyncAllocation>>>,
50        ptr: usize,
51        size: Option<usize>,
52        generation: Option<u64>,
53    ) -> Option<AsyncAllocation> {
54        let entries = allocations.get_mut(&ptr)?;
55
56        // When the caller names a generation or size, only that binding may be
57        // closed: falling back to "the oldest entry" would destroy a newer binding
58        // (the exact bug this filter exists to prevent). Without a filter the
59        // release closes the oldest binding, the best available guess.
60        let index = match (generation, size) {
61            (Some(generation), _) => entries.iter().position(|a| a.generation == generation),
62            (None, Some(size)) => entries.iter().position(|a| a.size == size),
63            (None, None) => Some(0),
64        };
65        let Some(index) = index else {
66            tracing::debug!(
67                "unmatched async release: ptr={:#x}, size={:?}, generation={:?}",
68                ptr,
69                size,
70                generation
71            );
72            return None;
73        };
74
75        let closed = entries.remove(index);
76        if entries.is_empty() {
77            allocations.remove(&ptr);
78        }
79        Some(closed)
80    }
81
82    /// Apply deallocation stats and profile updates (blocking variant).
83    fn apply_deallocation_stats(&self, closed: Option<AsyncAllocation>) {
84        let Some(closed) = closed else {
85            return;
86        };
87        if closed.task_id != 0 {
88            if let Ok(mut profiles) = self.profiles.lock() {
89                if let Some(profile) = profiles.get_mut(&closed.task_id) {
90                    profile.record_deallocation(closed.size as u64);
91                }
92            } else {
93                tracing::error!("Failed to acquire profiles lock during track_deallocation");
94            }
95        }
96
97        if closed.size > 0 {
98            if let Ok(mut stats) = self.stats.lock() {
99                stats.active_memory = stats.active_memory.saturating_sub(closed.size);
100                stats.total_deallocations += 1;
101                stats.total_deallocated += closed.size as u64;
102            } else {
103                tracing::error!("Failed to acquire stats lock during track_deallocation");
104            }
105        }
106    }
107}