ntex_bytes/tracker.rs
1//! Memory attribution for buffer allocations.
2use std::sync::atomic::{AtomicUsize, Ordering::Acquire, Ordering::Relaxed, Ordering::Release};
3use std::{fmt, ptr::NonNull, sync::atomic::fence};
4
5use crate::stvec::abort;
6
7const MAX_REFS: usize = isize::MAX as usize;
8
9/// Shared counter of the memory held by buffer allocations.
10///
11/// An allocation attributed to a tracker charges its size, including the
12/// allocation header, until the allocation is freed or returned to the
13/// page cache. The tracker state lives while any handle or attributed
14/// allocation exists, so a handle can outlive the buffers and the
15/// buffers can outlive all handles.
16// `BytesMut::tracker` borrows the allocation header field as a handle
17#[cfg_attr(docsrs, doc(cfg(feature = "tracker")))]
18#[repr(transparent)]
19pub struct BytesTracker(NonNull<TrackerInner>);
20
21const _: () = assert!(size_of::<BytesTracker>() == size_of::<*const TrackerInner>());
22
23/// Point-in-time values of a [`BytesTracker`].
24///
25/// The counters are read one by one, the snapshot is exact only when
26/// no allocation is attributed or released concurrently.
27#[cfg_attr(docsrs, doc(cfg(feature = "tracker")))]
28#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
29#[non_exhaustive]
30pub struct BytesTrackerSnapshot {
31 /// Memory held by attributed allocations, in bytes.
32 pub live_bytes: usize,
33 /// Number of attributed allocations.
34 pub live_allocations: usize,
35}
36
37pub(crate) struct TrackerInner {
38 // owner handles + attributed allocations
39 refs: AtomicUsize,
40 // owner handles
41 handles: AtomicUsize,
42 live_bytes: AtomicUsize,
43}
44
45// SAFETY: the shared state consists of atomics only
46unsafe impl Send for BytesTracker {}
47// SAFETY: the shared state consists of atomics only
48unsafe impl Sync for BytesTracker {}
49
50impl BytesTracker {
51 /// Creates a new tracker.
52 pub fn new() -> Self {
53 let inner = Box::new(TrackerInner {
54 refs: AtomicUsize::new(1),
55 handles: AtomicUsize::new(1),
56 live_bytes: AtomicUsize::new(0),
57 });
58 BytesTracker(NonNull::from(Box::leak(inner)))
59 }
60
61 /// Returns the current values of the tracker.
62 pub fn snapshot(&self) -> BytesTrackerSnapshot {
63 let inner = self.inner();
64 let handles = inner.handles.load(Relaxed);
65 let refs = inner.refs.load(Relaxed);
66 BytesTrackerSnapshot {
67 live_bytes: inner.live_bytes.load(Relaxed),
68 // fields are read one by one, a racing clone can make it negative
69 live_allocations: refs.saturating_sub(handles),
70 }
71 }
72
73 /// Returns `true` if both handles refer to the same tracker.
74 pub fn ptr_eq(&self, other: &BytesTracker) -> bool {
75 self.0 == other.0
76 }
77
78 pub(crate) fn as_inner(&self) -> *const TrackerInner {
79 self.0.as_ptr()
80 }
81
82 fn inner(&self) -> &TrackerInner {
83 // SAFETY: the handle holds a reference
84 unsafe { self.0.as_ref() }
85 }
86}
87
88impl Default for BytesTracker {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl Clone for BytesTracker {
95 fn clone(&self) -> Self {
96 let inner = self.inner();
97 if inner.refs.fetch_add(1, Relaxed) > MAX_REFS {
98 abort();
99 }
100 inner.handles.fetch_add(1, Relaxed);
101 BytesTracker(self.0)
102 }
103}
104
105impl Drop for BytesTracker {
106 fn drop(&mut self) {
107 self.inner().handles.fetch_sub(1, Relaxed);
108 // SAFETY: the handle holds a reference
109 unsafe { TrackerInner::release(self.0.as_ptr()) }
110 }
111}
112
113impl fmt::Debug for BytesTracker {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 let snapshot = self.snapshot();
116 f.debug_struct("BytesTracker")
117 .field("live_bytes", &snapshot.live_bytes)
118 .field("live_allocations", &snapshot.live_allocations)
119 .finish()
120 }
121}
122
123impl TrackerInner {
124 /// Attributes an allocation of `size` bytes, it holds a reference
125 /// until [`TrackerInner::uncharge`].
126 pub(crate) fn charge(&self, size: usize) {
127 if self.refs.fetch_add(1, Relaxed) > MAX_REFS {
128 abort();
129 }
130 self.live_bytes.fetch_add(size, Relaxed);
131 }
132
133 /// Adjusts the size of an attributed allocation.
134 pub(crate) fn resize(&self, old: usize, new: usize) {
135 if new > old {
136 self.live_bytes.fetch_add(new - old, Relaxed);
137 } else {
138 self.live_bytes.fetch_sub(old - new, Relaxed);
139 }
140 }
141
142 /// Releases an allocation of `size` bytes.
143 ///
144 /// # Safety
145 ///
146 /// `this` must point to a live tracker and the caller must own
147 /// a reference taken by [`TrackerInner::charge`].
148 pub(crate) unsafe fn uncharge(this: *const TrackerInner, size: usize) {
149 // SAFETY: the caller owns a reference
150 unsafe { (*this).live_bytes.fetch_sub(size, Relaxed) };
151 // SAFETY: the caller owns a reference
152 unsafe { TrackerInner::release(this) }
153 }
154
155 /// # Safety
156 ///
157 /// The caller must own a reference, it is consumed.
158 unsafe fn release(this: *const TrackerInner) {
159 // SAFETY: the caller owns a reference
160 if unsafe { (*this).refs.fetch_sub(1, Release) } != 1 {
161 return;
162 }
163 fence(Acquire);
164 // SAFETY: last reference, the state was created by Box in BytesTracker::new
165 drop(unsafe { Box::from_raw(this.cast_mut()) });
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn snap(bytes: usize, allocs: usize) -> BytesTrackerSnapshot {
174 BytesTrackerSnapshot {
175 live_bytes: bytes,
176 live_allocations: allocs,
177 }
178 }
179
180 /// STATEMENT: Tracker charges and resizes update live bytes while allocation counts follow allocation lifetimes.
181 ///
182 /// HOW:
183 /// - Charge 100 and 50 bytes, grow and shrink one charge, and check snapshots after each operation.
184 /// - Uncharge both allocations and verify zero totals and the empty debug representation.
185 #[test]
186 fn charge_resize_uncharge() {
187 let tracker = BytesTracker::new();
188 assert_eq!(tracker.snapshot(), snap(0, 0));
189
190 let inner = tracker.as_inner();
191 // SAFETY: the tracker is alive
192 unsafe { (*inner).charge(100) };
193 // SAFETY: The tracker handle keeps `inner` alive while this additional allocation is
194 // charged.
195 unsafe { (*inner).charge(50) };
196 assert_eq!(tracker.snapshot(), snap(150, 2));
197
198 // SAFETY: The tracker handle keeps `inner` alive while its existing charge is resized.
199 unsafe { (*inner).resize(100, 300) };
200 assert_eq!(tracker.snapshot(), snap(350, 2));
201 // SAFETY: The tracker handle keeps `inner` alive while its existing charge is resized.
202 unsafe { (*inner).resize(300, 200) };
203 // SAFETY: The tracker handle keeps `inner` alive while its existing charge is resized.
204 unsafe { (*inner).resize(200, 200) };
205 assert_eq!(tracker.snapshot(), snap(250, 2));
206
207 // SAFETY: The first charge retains a reference and now accounts for 200 bytes.
208 unsafe { TrackerInner::uncharge(inner, 200) };
209 assert_eq!(tracker.snapshot(), snap(50, 1));
210 // SAFETY: The second charge retains a reference and accounts for 50 bytes.
211 unsafe { TrackerInner::uncharge(inner, 50) };
212 assert_eq!(tracker.snapshot(), snap(0, 0));
213 assert_eq!(
214 format!("{tracker:?}"),
215 "BytesTracker { live_bytes: 0, live_allocations: 0 }"
216 );
217 }
218
219 /// STATEMENT: Cloned tracker handles share accounting without counting handles as allocations.
220 ///
221 /// HOW:
222 /// - Compare tracker identities, charge ten bytes, and observe the charge through a clone.
223 /// - Drop the clone, check the charge persists, and uncharge to return the original snapshot to zero.
224 #[test]
225 fn handles() {
226 let tracker = BytesTracker::default();
227 let tracker2 = tracker.clone();
228 assert!(tracker.ptr_eq(&tracker2));
229 assert!(!tracker.ptr_eq(&BytesTracker::new()));
230
231 // handles are not allocations
232 assert_eq!(tracker.snapshot(), snap(0, 0));
233 // SAFETY: The tracker handle keeps its state alive while the allocation is charged.
234 unsafe { (*tracker.as_inner()).charge(10) };
235 assert_eq!(tracker2.snapshot(), snap(10, 1));
236 drop(tracker2);
237 assert_eq!(tracker.snapshot(), snap(10, 1));
238 // SAFETY: The ten-byte charge retains the reference consumed by this uncharge.
239 unsafe { TrackerInner::uncharge(tracker.as_inner(), 10) };
240 assert_eq!(tracker.snapshot(), snap(0, 0));
241 }
242
243 /// STATEMENT: Outstanding allocation charges keep tracker state alive after all public handles drop.
244 ///
245 /// HOW:
246 /// - Charge ten and twenty bytes, drop the tracker handle, and release both charges through the saved inner pointer.
247 #[test]
248 fn allocation_outlives_handles() {
249 let tracker = BytesTracker::new();
250 let inner = tracker.as_inner();
251 // SAFETY: The tracker handle keeps `inner` alive while the allocation is charged.
252 unsafe { (*inner).charge(10) };
253 // SAFETY: The tracker handle keeps `inner` alive while the additional allocation is
254 // charged.
255 unsafe { (*inner).charge(20) };
256 drop(tracker);
257 // the allocations keep the state alive, the last one frees it
258 // SAFETY: The ten-byte charge keeps `inner` alive and supplies the reference being
259 // released.
260 unsafe { TrackerInner::uncharge(inner, 10) };
261 // SAFETY: The twenty-byte charge keeps `inner` alive and supplies the final reference being
262 // released.
263 unsafe { TrackerInner::uncharge(inner, 20) };
264 }
265
266 /// STATEMENT: Concurrent tracker operations retain accurate totals and allow charges to outlive thread handles.
267 ///
268 /// HOW:
269 /// - Run four threads repeatedly cloning handles, charging, resizing, and uncharging allocations.
270 /// - Join them, verify one byte and allocation per thread remain, then drop the last handle and release those charges.
271 #[test]
272 fn concurrent() {
273 const THREADS: usize = 4;
274 const ITERS: usize = if cfg!(miri) { 20 } else { 10_000 };
275
276 let tracker = BytesTracker::new();
277 let threads: Vec<_> = (0..THREADS)
278 .map(|_| {
279 let tracker = tracker.clone();
280 std::thread::spawn(move || {
281 let inner = tracker.as_inner();
282 for i in 0..ITERS {
283 let handle = tracker.clone();
284 // SAFETY: The thread owns a tracker handle that keeps `inner` alive while
285 // charging.
286 unsafe { (*inner).charge(i) };
287 // SAFETY: The thread owns a tracker handle and resizes its own existing
288 // charge.
289 unsafe { (*inner).resize(i, i + 1) };
290 drop(handle);
291 // SAFETY: The loop iteration owns the charge and its reference, resized
292 // above to `i + 1` bytes.
293 unsafe { TrackerInner::uncharge(inner, i + 1) };
294 }
295 // left for the main thread to release
296 // SAFETY: The thread still owns a tracker handle; the new charge retains a
297 // reference for the main thread.
298 unsafe { (*inner).charge(1) };
299 })
300 })
301 .collect();
302 for t in threads {
303 t.join().unwrap();
304 }
305 assert_eq!(tracker.snapshot(), snap(THREADS, THREADS));
306
307 let inner = tracker.as_inner();
308 drop(tracker);
309 for _ in 0..THREADS {
310 // SAFETY: Each joined thread left one one-byte charge and a reference, consumed once
311 // per iteration.
312 unsafe { TrackerInner::uncharge(inner, 1) };
313 }
314 }
315}