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;