Skip to main content

shuyi_alloc_count/counter/
tagged.rs

1//! [`CountingAlloc::with_tags`]: a counter that also counts by memory tag
2//! ([`super::tags`]), each block carrying its tag in a header in front of
3//! it.
4
5use alloc::vec::Vec;
6use core::alloc::{GlobalAlloc, Layout};
7use core::ptr;
8
9use super::tags::{self, TAGS};
10use super::{CountingAlloc, Word};
11use crate::TagCounts;
12
13/// What a [`CountingAlloc::with_tags`] counts by tag: for each of the
14/// [`TAGS`], its live bytes and blocks, the bytes ever allocated under
15/// it, and the peak of its live bytes.
16///
17/// **The header.** Each block carries the tag it counts under in a header
18/// in front of it of `max(align, 8)` bytes, for the block's alignment
19/// `align`: the block keeps its alignment, and the tag (a `u16`) sits in the header's
20/// last two bytes, just before the block. The inner allocator is asked
21/// for the block and its header, at the block's alignment. The counts
22/// are the bytes asked for: a header's bytes are never counted.
23pub struct Tagged {
24    tags: [TagWords; TAGS],
25}
26
27impl core::fmt::Debug for Tagged {
28    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29        f.debug_struct("Tagged").finish_non_exhaustive()
30    }
31}
32
33/// One tag's counts.
34struct TagWords {
35    live: Word,
36    blocks: Word,
37    allocated: Word,
38    peak: Word,
39}
40
41impl TagWords {
42    const fn new() -> Self {
43        Self {
44            live: Word::new(),
45            blocks: Word::new(),
46            allocated: Word::new(),
47            peak: Word::new(),
48        }
49    }
50}
51
52impl Tagged {
53    const fn new() -> Self {
54        Self {
55            tags: [const { TagWords::new() }; TAGS],
56        }
57    }
58
59    /// A block of `size` bytes now counts under `tag`.
60    fn add(&self, tag: u16, size: usize) {
61        let words = &self.tags[tags::slot(tag)];
62        let live = words.live.add(size);
63        words.peak.max(live);
64        words.blocks.add(1);
65        words.allocated.add(size);
66    }
67
68    /// A block of `size` bytes no longer counts under `tag`.
69    fn remove(&self, tag: u16, size: usize) {
70        let words = &self.tags[tags::slot(tag)];
71        words.live.sub(size);
72        words.blocks.sub(1);
73    }
74}
75
76/// The header in front of a block of alignment `align`: `align` bytes, or
77/// 8 if that is less, so the block after it keeps its alignment.
78pub(crate) fn header_bytes(align: usize) -> usize {
79    align.max(8)
80}
81
82/// The layout asked of the inner allocator for `layout`'s block and its
83/// header, and the header's bytes; `None` if that is too big for a
84/// layout.
85fn outer(layout: Layout) -> Option<(Layout, usize)> {
86    let header = header_bytes(layout.align());
87    let size = layout.size().checked_add(header)?;
88    let outer = Layout::from_size_align(size, layout.align()).ok()?;
89    Some((outer, header))
90}
91
92/// Writes `tag` in the header in front of `block`.
93///
94/// # Safety
95///
96/// `block` is a block of this module's, with its header in front of it.
97unsafe fn write_tag(block: *mut u8, tag: u16) {
98    // SAFETY: the header is at least 8 bytes in front of `block`, inside
99    // the inner allocator's block, so its last two are ours to write; they
100    // may be unaligned (a block of alignment 1).
101    unsafe { block.sub(2).cast::<u16>().write_unaligned(tag) };
102}
103
104/// The tag in the header in front of `block`.
105///
106/// # Safety
107///
108/// As [`write_tag`], with a tag written.
109unsafe fn read_tag(block: *mut u8) -> u16 {
110    // SAFETY: as `write_tag`'s, and `write_tag` wrote these two bytes.
111    unsafe { block.sub(2).cast::<u16>().read_unaligned() }
112}
113
114impl<A> CountingAlloc<A, Tagged> {
115    /// A counter around `inner` that also counts by memory tag
116    /// ([`tags`](crate::tags)): each block counts under the tag current
117    /// on its thread when it was allocated, or last reallocated. It costs
118    /// a header in front of each block ([`Tagged`]).
119    pub const fn with_tags(inner: A) -> Self {
120        Self {
121            inner,
122            live: Word::new(),
123            peak: Word::new(),
124            allocs: Word::new(),
125            tags: Tagged::new(),
126        }
127    }
128
129    /// Every tag with bytes live or ever allocated under it, by number,
130    /// with its class, name and counts. It allocates the list: never call
131    /// it from inside an allocator.
132    pub fn rows(&self) -> Vec<TagCounts> {
133        let names = tags::names();
134        let wide = |n: usize| u64::try_from(n).unwrap_or(u64::MAX);
135        let mut rows = Vec::new();
136        for ((tag, words), (class, name)) in (0u16..).zip(&self.tags.tags).zip(names) {
137            let (live, allocated) = (words.live.get(), words.allocated.get());
138            if live == 0 && allocated == 0 {
139                continue;
140            }
141            let mut row = TagCounts::new(tag, class, name);
142            row.live_bytes = wide(live);
143            row.live_blocks = u32::try_from(words.blocks.get()).unwrap_or(u32::MAX);
144            row.allocated_bytes = wide(allocated);
145            row.peak_bytes = wide(words.peak.get());
146            rows.push(row);
147        }
148        rows
149    }
150}
151
152impl<A: GlobalAlloc> CountingAlloc<A, Tagged> {
153    /// A block for `layout` behind its header, from `inner` (zeroed or
154    /// not), tagged with this thread's current tag and counted.
155    ///
156    /// # Safety
157    ///
158    /// `alloc`'s contract: `layout`'s size is above zero.
159    unsafe fn alloc_tagged(&self, layout: Layout, zeroed: bool) -> *mut u8 {
160        self.allocs.add(1);
161        let Some((outer, header)) = outer(layout) else {
162            return ptr::null_mut();
163        };
164        // SAFETY: `outer` is bigger than `layout`, so above zero.
165        let base = unsafe {
166            if zeroed {
167                self.inner.alloc_zeroed(outer)
168            } else {
169                self.inner.alloc(outer)
170            }
171        };
172        if base.is_null() {
173            return base;
174        }
175        // SAFETY: `base` has `header` bytes and the block's after them.
176        let block = unsafe { base.add(header) };
177        let tag = tags::current();
178        // SAFETY: `block` has its header in front of it.
179        unsafe { write_tag(block, tag) };
180        self.grew(layout.size());
181        self.tags.add(tag, layout.size());
182        block
183    }
184}
185
186// SAFETY: every block handed out is inside a block `inner` gave for a
187// layout of the same alignment, `header_bytes` after its start: a multiple
188// of the alignment, so the block is aligned, and the block's size fits
189// after it. Each call gives `inner` back the start and layout it gave.
190unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAlloc<A, Tagged> {
191    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
192        // SAFETY: the caller keeps `alloc`'s contract.
193        unsafe { self.alloc_tagged(layout, false) }
194    }
195
196    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
197        // SAFETY: as `alloc`.
198        unsafe { self.alloc_tagged(layout, true) }
199    }
200
201    unsafe fn dealloc(&self, block: *mut u8, layout: Layout) {
202        // Never a block of this allocator's (the caller's contract), and
203        // with no header in front of it to read.
204        if block.is_null() {
205            return;
206        }
207        // A block allocated with `layout` had an outer layout.
208        let Some((outer, header)) = outer(layout) else {
209            return;
210        };
211        // SAFETY: the caller says `block` came from this allocator with
212        // `layout`, so `alloc_tagged` gave it, with its header, `header`
213        // bytes after the start `inner` gave for `outer`.
214        let tag = unsafe { read_tag(block) };
215        // SAFETY: as above: the start and layout `inner` gave.
216        unsafe { self.inner.dealloc(block.sub(header), outer) };
217        self.live.sub(layout.size());
218        self.tags.remove(tag, layout.size());
219    }
220
221    unsafe fn realloc(&self, block: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
222        self.allocs.add(1);
223        // As `dealloc`'s: no block, and no header to read.
224        if block.is_null() {
225            return ptr::null_mut();
226        }
227        let Some((outer, header)) = outer(layout) else {
228            return ptr::null_mut();
229        };
230        // The new outer size must make a layout too, or nothing changes.
231        let new_outer = new_size.checked_add(header);
232        let Some(new_outer) =
233            new_outer.filter(|&n| Layout::from_size_align(n, layout.align()).is_ok())
234        else {
235            return ptr::null_mut();
236        };
237        // SAFETY: as `dealloc`'s: `block` is this allocator's, with
238        // `layout`, `header` bytes after the start `inner` gave for
239        // `outer`; and `new_outer` makes a valid layout at its alignment.
240        let old_tag = unsafe { read_tag(block) };
241        // SAFETY: as above.
242        let base = unsafe { self.inner.realloc(block.sub(header), outer, new_outer) };
243        if base.is_null() {
244            // The block is as it was, under its tag.
245            return base;
246        }
247        // SAFETY: `inner` kept the header's bytes and the block's (up to
248        // the smaller size) at `base`, so the block is `header` after it.
249        let moved = unsafe { base.add(header) };
250        let tag = tags::current();
251        // SAFETY: `moved` has its header in front of it.
252        unsafe { write_tag(moved, tag) };
253        self.resized(layout.size(), new_size);
254        self.tags.remove(old_tag, layout.size());
255        self.tags.add(tag, new_size);
256        moved
257    }
258}