Skip to main content

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}