Skip to main content

shuyi_alloc_count/
counter.rs

1//! [`CountingAlloc`], its memory tags and the words it counts in: this
2//! crate's only `unsafe` (ADR-0046 ยง9), each block with the reason it
3//! holds.
4
5use core::alloc::{GlobalAlloc, Layout};
6
7use crate::Counts;
8
9mod tagged;
10pub mod tags;
11
12pub use tagged::Tagged;
13
14/// A global allocator around `inner` that counts what is asked of it: the
15/// bytes live, their peak, and the calls to `alloc`, `alloc_zeroed` and
16/// `realloc`. It counts each [`Layout`]'s size, never the inner
17/// allocator's own overhead.
18///
19/// Make it a `static`, with `#[global_allocator]` to count a whole
20/// program, or called directly as a test does. A program with
21/// `shuyi-app` or `shuyi-cli` already has one (the `global` module says
22/// which).
23///
24/// ```
25/// use shuyi_alloc_count::CountingAlloc;
26/// use std::alloc::System;
27///
28/// static HEAP: CountingAlloc<System> = CountingAlloc::new(System);
29///
30/// assert_eq!(HEAP.live_bytes(), 0);
31/// ```
32///
33/// [`CountingAlloc::new`] counts the whole; [`CountingAlloc::with_tags`]
34/// also counts by memory tag ([`tags`]), with a header in front of each
35/// block ([`Tagged`]).
36///
37/// On a target with atomics each count is one atomic read-modify-write, so
38/// any thread may allocate; on one without (an old ARM console) the
39/// counts are plain cells, and the target has one thread.
40#[derive(Debug)]
41pub struct CountingAlloc<A, T = Untagged> {
42    inner: A,
43    live: Word,
44    peak: Word,
45    allocs: Word,
46    tags: T,
47}
48
49/// What a [`CountingAlloc::new`] counts by tag: nothing, and no header in
50/// front of a block.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct Untagged;
53
54impl<A> CountingAlloc<A> {
55    /// A counter around `inner`, nothing counted yet. It counts the whole,
56    /// with no header in front of a block: for a bench, and a console that
57    /// counts its heap without tags.
58    pub const fn new(inner: A) -> Self {
59        Self {
60            inner,
61            live: Word::new(),
62            peak: Word::new(),
63            allocs: Word::new(),
64            tags: Untagged,
65        }
66    }
67}
68
69impl<A, T> CountingAlloc<A, T> {
70    /// The bytes allocated and not yet freed.
71    pub fn live_bytes(&self) -> usize {
72        self.live.get()
73    }
74
75    /// The most bytes live at once since the start or the last
76    /// [`reset_peak`](Self::reset_peak).
77    pub fn peak_bytes(&self) -> usize {
78        self.peak.get()
79    }
80
81    /// Starts the peak again from the bytes live now. A tag's peak
82    /// ([`TagCounts::peak_bytes`](crate::TagCounts::peak_bytes)) stays.
83    pub fn reset_peak(&self) {
84        self.peak.set(self.live.get());
85    }
86
87    /// The calls to `alloc`, `alloc_zeroed` and `realloc` since the last
88    /// take (or the start), read and reset in one step; at most
89    /// `u32::MAX`.
90    pub fn take_allocs(&self) -> u32 {
91        u32::try_from(self.allocs.take()).unwrap_or(u32::MAX)
92    }
93
94    /// All three: the live bytes, their peak, and the calls, taken
95    /// ([`take_allocs`](Self::take_allocs)).
96    pub fn counts(&self) -> Counts {
97        let wide = |n: usize| u64::try_from(n).unwrap_or(u64::MAX);
98        Counts::new(
99            wide(self.live_bytes()),
100            wide(self.peak_bytes()),
101            self.take_allocs(),
102        )
103    }
104
105    /// Counts a call that asked for `size` bytes and got `ptr`.
106    fn allocated(&self, ptr: *mut u8, size: usize) {
107        self.allocs.add(1);
108        if !ptr.is_null() {
109            self.grew(size);
110        }
111    }
112
113    /// `size` more bytes live, and the peak raised to them.
114    fn grew(&self, size: usize) {
115        let live = self.live.add(size);
116        self.peak.max(live);
117    }
118
119    /// A block of `old` bytes now `new` bytes: the live bytes moved by the
120    /// difference.
121    fn resized(&self, old: usize, new: usize) {
122        if new >= old {
123            self.grew(new - old);
124        } else {
125            self.live.sub(old - new);
126        }
127    }
128}
129
130// SAFETY: every method hands its call to `inner` unchanged, under the
131// caller's own guarantees, and only counts around it: the memory it gives
132// is `inner`'s, as `inner` gives it.
133unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAlloc<A> {
134    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
135        // SAFETY: the caller keeps `alloc`'s contract (a size above zero),
136        // which is all `inner.alloc` asks.
137        let ptr = unsafe { self.inner.alloc(layout) };
138        self.allocated(ptr, layout.size());
139        ptr
140    }
141
142    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
143        // SAFETY: as `alloc`: the caller's layout, unchanged.
144        let ptr = unsafe { self.inner.alloc_zeroed(layout) };
145        self.allocated(ptr, layout.size());
146        ptr
147    }
148
149    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
150        // SAFETY: the caller says `ptr` came from this allocator, so from
151        // `inner`, with this `layout`.
152        unsafe { self.inner.dealloc(ptr, layout) };
153        self.live.sub(layout.size());
154    }
155
156    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
157        // SAFETY: the caller says `ptr` came from this allocator, so from
158        // `inner`, with `layout`, and that `new_size` is valid for it.
159        let moved = unsafe { self.inner.realloc(ptr, layout, new_size) };
160        self.allocs.add(1);
161        if !moved.is_null() {
162            self.resized(layout.size(), new_size);
163        }
164        moved
165    }
166}
167
168/// One count, a machine word, on a target with atomics: each change one
169/// atomic read-modify-write.
170#[cfg(target_has_atomic = "ptr")]
171#[derive(Debug)]
172struct Word(core::sync::atomic::AtomicUsize);
173
174#[cfg(target_has_atomic = "ptr")]
175impl Word {
176    const fn new() -> Self {
177        Self(core::sync::atomic::AtomicUsize::new(0))
178    }
179
180    fn get(&self) -> usize {
181        self.0.load(core::sync::atomic::Ordering::Relaxed)
182    }
183
184    fn set(&self, n: usize) {
185        self.0.store(n, core::sync::atomic::Ordering::Relaxed);
186    }
187
188    /// Adds `n`, wrapping, and gives the new count.
189    fn add(&self, n: usize) -> usize {
190        let old = self.0.fetch_add(n, core::sync::atomic::Ordering::Relaxed);
191        old.wrapping_add(n)
192    }
193
194    fn sub(&self, n: usize) {
195        self.0.fetch_sub(n, core::sync::atomic::Ordering::Relaxed);
196    }
197
198    /// Raises the count to `n`, if `n` is more.
199    fn max(&self, n: usize) {
200        self.0.fetch_max(n, core::sync::atomic::Ordering::Relaxed);
201    }
202
203    /// The count, and 0 in its place.
204    fn take(&self) -> usize {
205        self.0.swap(0, core::sync::atomic::Ordering::Relaxed)
206    }
207}
208
209#[cfg(not(target_has_atomic = "ptr"))]
210use single::Word;
211
212/// One count as a plain cell, for a target without atomics: such a target
213/// (the GBA, the DS) runs one thread, so no two threads ever share it.
214/// Compiled for the tests too, which call it from one thread.
215#[cfg(any(test, not(target_has_atomic = "ptr")))]
216mod single {
217    use core::cell::Cell;
218
219    #[derive(Debug)]
220    pub(super) struct Word(Cell<usize>);
221
222    // SAFETY: only a target without atomics shares it, as a `static`'s
223    // count, and such a target has one thread: no two threads ever reach
224    // the cell.
225    #[cfg(not(target_has_atomic = "ptr"))]
226    unsafe impl Sync for Word {}
227
228    impl Word {
229        pub(super) const fn new() -> Self {
230            Self(Cell::new(0))
231        }
232
233        pub(super) fn get(&self) -> usize {
234            self.0.get()
235        }
236
237        pub(super) fn set(&self, n: usize) {
238            self.0.set(n);
239        }
240
241        /// Adds `n`, wrapping, and gives the new count.
242        pub(super) fn add(&self, n: usize) -> usize {
243            let new = self.0.get().wrapping_add(n);
244            self.0.set(new);
245            new
246        }
247
248        pub(super) fn sub(&self, n: usize) {
249            self.0.set(self.0.get().wrapping_sub(n));
250        }
251
252        /// Raises the count to `n`, if `n` is more.
253        pub(super) fn max(&self, n: usize) {
254            self.0.set(self.0.get().max(n));
255        }
256
257        /// The count, and 0 in its place.
258        pub(super) fn take(&self) -> usize {
259            self.0.replace(0)
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests;