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}