Skip to main content

memscope_rs/tracker/
release.rs

1//! Release and reallocation bookkeeping for the unified facade.
2//!
3//! Split out of `tracker.rs` to keep that file within the project line budget. These are
4//! `impl Tracker` items and share its private state; nothing here is a new API surface.
5//!
6//! Every function follows the same rule: the event must describe the binding that was
7//! actually closed (`ClosedBinding`), and the deduplication state is keyed by binding
8//! identity, so closing one generation never erases the state of a surviving one.
9
10use super::*;
11
12impl Tracker {
13    // Note: AsyncTracker profile updates for deallocations and reallocations
14    // are NOT pushed from the release path. The export path
15    // (`export_async_analysis_json`) rebuilds task profiles from the event
16    // stream, so the `EventStore` events recorded here are the authoritative
17    // source. Attempting to update the live AsyncTracker from allocator
18    // callbacks would require acquiring its internal locks, which can deadlock
19    // with `shutdown()` or concurrent allocations.
20
21    pub(super) fn recorded_slot(&self, ptr: usize) -> RecordedSlot {
22        self.recorded_bindings
23            .entry(ptr)
24            .or_insert_with(|| Arc::new(Mutex::new(RecordedState::new())))
25            .clone()
26    }
27
28    /// Drop the slot of a released address once nobody else can be using it.
29    ///
30    /// Slots must stay stable while an address is live, but keeping one entry per
31    /// address ever allocated makes the profiler itself grow without bound in a
32    /// long-running process. `remove_if` holds the shard write lock, so no thread can
33    /// obtain the slot while it is inspected; a strong count of one means the map is
34    /// its only owner, and `try_lock` (never blocking) then confirms it is empty.
35    pub(super) fn reclaim_slot(&self, ptr: usize) {
36        self.recorded_bindings.remove_if(&ptr, |_, slot| {
37            Arc::strong_count(slot) == 1
38                && slot
39                    .try_lock()
40                    .map(|state| state.is_empty())
41                    .unwrap_or(false)
42        });
43    }
44
45    pub fn track_deallocation(&self, ptr: usize) -> crate::TrackingResult<bool> {
46        let _gate = crate::core::allocator::capture_gate::acquire(ptr);
47
48        // Nothing tracked at this address: report it without allocating a slot, otherwise
49        // every untracked release (including allocations the profiler itself suppressed)
50        // leaves an empty deduplication slot behind.
51        if self.inner.live_binding_count_at(ptr) == 0 {
52            return Ok(false);
53        }
54
55        let slot = self.recorded_slot(ptr);
56        let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
57
58        // Name the binding when the address holds exactly one: that is an exact release.
59        // With several live bindings only the address is known, so the cache closes the
60        // oldest one and counts the guess.
61        let serial = if self.inner.live_binding_count_at(ptr) == 1 {
62            self.inner.binding_serial(ptr)
63        } else {
64            None
65        };
66        let degraded = serial.is_none();
67        let closed = self.inner.close_and_report(ptr, serial)?;
68
69        // The event must describe the binding that was actually closed: with an ambiguous
70        // release the newest binding is not the one that disappeared, and publishing its
71        // serial would make replay close the wrong row.
72        if let Some(closed) = closed {
73            // Remove exactly the identity that was closed: a degraded release closes the
74            // oldest binding, and the surviving one must keep its dedupe state or the next
75            // observation of that block would publish a second allocation.
76            guard.remove(&closed.serial);
77            self.clear_allocator_charge(ptr, closed.serial);
78            let mut event = MemoryEvent::deallocate(
79                closed.ptr,
80                closed.size,
81                crate::utils::current_thread_id_u64(),
82            );
83            event.binding_serial = Some(closed.serial);
84            event.degraded_release = degraded;
85            self.event_store.record(event);
86
87            // AsyncTracker live-state updates are NOT done here. Calling
88            // notify_deallocation synchronously from the allocator's dealloc
89            // callback re-enters AsyncTracker locks, which deadlocks with
90            // shutdown(), concurrent track_allocation, and multi-thread
91            // scenarios. The event-stream replay (rebuild_with_live_tracker)
92            // is the authoritative source for exports and dashboards.
93            // The live AsyncTracker state may be slightly stale (showing
94            // allocations without matching releases), but it cannot deadlock.
95        }
96
97        let cleared = guard.is_empty();
98        drop(guard);
99        drop(slot);
100        if cleared {
101            self.reclaim_slot(ptr);
102        }
103
104        Ok(closed.is_some())
105    }
106
107    /// Release one specific binding and publish the matching event.
108    ///
109    /// Exact entry point: the caller names the identity, so no policy guess is involved
110    /// and the published event carries that identity. Returns `false` when the binding is
111    /// already gone (a late release of a superseded generation).
112    pub fn track_deallocation_of(&self, ptr: usize, serial: u64) -> crate::TrackingResult<bool> {
113        let _gate = crate::core::allocator::capture_gate::acquire(ptr);
114        let slot = self.recorded_slot(ptr);
115        let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
116
117        let closed = self.inner.close_and_report(ptr, Some(serial))?;
118        if let Some(closed) = closed {
119            guard.remove(&closed.serial);
120            self.clear_allocator_charge(ptr, closed.serial);
121            let mut event = MemoryEvent::deallocate(
122                closed.ptr,
123                closed.size,
124                crate::utils::current_thread_id_u64(),
125            );
126            event.binding_serial = Some(closed.serial);
127            self.event_store.record(event);
128
129            // AsyncTracker live-state updates are NOT done here — see the
130            // comment in track_deallocation above.
131        }
132
133        let cleared = guard.is_empty();
134        drop(guard);
135        drop(slot);
136        if cleared {
137            self.reclaim_slot(ptr);
138        }
139        Ok(closed.is_some())
140    }
141
142    /// Track a reallocation of a tracked block.
143    ///
144    /// The block keeps its identity (and its binding serial): the new size replaces the old
145    /// one and the allocation counter does not change, whether the allocator kept the
146    /// address or moved the block. The dedupe state moves with the binding, so a later
147    /// release still closes the identity it belongs to.
148    pub fn track_reallocation(
149        &self,
150        old_ptr: usize,
151        new_ptr: usize,
152        old_size: usize,
153        new_size: usize,
154    ) {
155        let _gate = crate::core::allocator::capture_gate::acquire(old_ptr);
156        let old_slot = self.recorded_slot(old_ptr);
157
158        if old_ptr == new_ptr {
159            let mut guard = old_slot.lock().unwrap_or_else(|e| e.into_inner());
160            if let Some((serial, binding)) =
161                self.record_reallocation(old_ptr, new_ptr, old_size, new_size)
162            {
163                guard.insert(serial, binding);
164            }
165            // AsyncTracker profile updates are handled at export time
166            // via event replay. The reallocation event in the EventStore
167            // carries the task_id, old_size, and new_size.
168            return;
169        }
170
171        let new_slot = self.recorded_slot(new_ptr);
172        // Address order prevents opposing migrations from acquiring these locks in reverse.
173        let (mut old_guard, mut new_guard) = if old_ptr < new_ptr {
174            let old = old_slot.lock().unwrap_or_else(|e| e.into_inner());
175            let new = new_slot.lock().unwrap_or_else(|e| e.into_inner());
176            (old, new)
177        } else {
178            let new = new_slot.lock().unwrap_or_else(|e| e.into_inner());
179            let old = old_slot.lock().unwrap_or_else(|e| e.into_inner());
180            (old, new)
181        };
182
183        let binding = self.record_reallocation(old_ptr, new_ptr, old_size, new_size);
184        if let Some((serial, state)) = binding {
185            // Move only the identity that was reallocated: the address may still hold
186            // other live bindings, and their dedupe state must survive.
187            old_guard.remove(&serial);
188            new_guard.insert(serial, state);
189        }
190        drop(old_guard);
191        drop(new_guard);
192        drop(old_slot);
193        drop(new_slot);
194        if binding.is_some() {
195            self.reclaim_slot(old_ptr);
196        }
197
198        // AsyncTracker profile updates are handled at export time
199        // via event replay. The reallocation event in the EventStore
200        // carries the task_id, old_size, and new_size.
201    }
202
203    /// Publish a reallocation by looking the old binding up in the core cache.
204    ///
205    /// Called with the affected address slots locked through event publication.
206    pub(super) fn record_reallocation(
207        &self,
208        old_ptr: usize,
209        new_ptr: usize,
210        old_size: usize,
211        new_size: usize,
212    ) -> Option<(u64, RecordedBinding)> {
213        if let Err(e) = self
214            .inner
215            .track_reallocation(old_ptr, new_ptr, old_size, new_size)
216        {
217            tracing::error!(
218                "Failed to track reallocation {:x} -> {:x}: {}",
219                old_ptr,
220                new_ptr,
221                e
222            );
223            return None;
224        }
225
226        // The reallocation event carries no type information, so the binding is remembered
227        // as *untyped*: the next `track!` on it must attach the type instead of being
228        // treated as a duplicate observation.
229        let serial = self.inner.binding_serial(new_ptr).unwrap_or(0);
230        self.migrate_allocator_charge(old_ptr, new_ptr, serial);
231        let thread_id_u64 = crate::utils::current_thread_id_u64();
232        let mut event =
233            MemoryEvent::reallocate_moved(old_ptr, new_ptr, old_size, new_size, thread_id_u64);
234        event.binding_serial = Some(serial);
235        self.event_store.record(event);
236        Some((
237            serial,
238            RecordedBinding {
239                size: new_size,
240                typed: false,
241                charged: true,
242            },
243        ))
244    }
245}