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}