1pub(crate) const CLASS_COUNT: usize = 33;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct SizeClass {
26 pub min_bytes: u64,
28 pub max_bytes: u64,
30 pub allocs: u64,
32 pub live_blocks: u64,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SizeClasses {
39 classes: [SizeClass; CLASS_COUNT],
40}
41
42impl SizeClasses {
43 pub fn classes(&self) -> &[SizeClass; CLASS_COUNT] {
44 &self.classes
45 }
46
47 pub fn busiest(&self) -> Option<SizeClass> {
50 self.classes
51 .iter()
52 .copied()
53 .filter(|c| c.live_blocks > 0)
54 .max_by_key(|c| c.live_blocks)
55 }
56
57 pub fn live_blocks(&self) -> u64 {
58 self.classes.iter().map(|c| c.live_blocks).sum()
59 }
60}
61
62#[cfg(any(feature = "detail", test))]
64const fn class_of(size: usize) -> usize {
65 let bits = (usize::BITS - size.leading_zeros()) as usize;
66 if bits >= CLASS_COUNT {
67 CLASS_COUNT - 1
68 } else {
69 bits
70 }
71}
72
73#[cfg(any(feature = "detail", test))]
75const fn class_bounds(class: usize) -> (u64, u64) {
76 match class {
77 0 => (0, 0),
78 c if c == CLASS_COUNT - 1 => (1 << (CLASS_COUNT - 2), u64::MAX),
79 c => (1 << (c - 1), (1 << c) - 1),
80 }
81}
82
83#[cfg(not(feature = "detail"))]
84mod imp {
85 pub(crate) fn record_alloc(_size: usize) {}
86 pub(crate) fn record_free(_size: usize) {}
87 pub(crate) fn record_realloc(_old_size: usize, _new_size: usize) {}
88 pub(crate) fn snapshot() -> Option<super::SizeClasses> {
89 None
90 }
91}
92
93#[cfg(feature = "detail")]
94mod imp {
95 use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
96
97 use super::{CLASS_COUNT, SizeClass, SizeClasses, class_bounds, class_of};
98
99 struct Class {
100 allocs: AtomicU64,
101 live: AtomicU64,
102 }
103
104 impl Class {
105 const fn new() -> Self {
106 Self {
107 allocs: AtomicU64::new(0),
108 live: AtomicU64::new(0),
109 }
110 }
111 }
112
113 pub(crate) struct Histogram {
118 classes: [Class; CLASS_COUNT],
119 }
120
121 impl Histogram {
122 pub(crate) const fn new() -> Self {
123 Self {
124 classes: [const { Class::new() }; CLASS_COUNT],
125 }
126 }
127
128 pub(crate) fn record_alloc(&self, size: usize) {
129 let class = &self.classes[class_of(size)];
130 class.allocs.fetch_add(1, Relaxed);
131 class.live.fetch_add(1, Relaxed);
132 }
133
134 pub(crate) fn record_free(&self, size: usize) {
135 let live = &self.classes[class_of(size)].live;
136 let _ = live.fetch_update(Relaxed, Relaxed, |n| Some(n.saturating_sub(1)));
137 }
138
139 pub(crate) fn record_realloc(&self, old_size: usize, new_size: usize) {
142 let (old, new) = (class_of(old_size), class_of(new_size));
143 if old != new {
144 self.record_free(old_size);
145 self.classes[new].live.fetch_add(1, Relaxed);
146 }
147 }
148
149 pub(crate) fn snapshot(&self) -> SizeClasses {
150 SizeClasses {
151 classes: core::array::from_fn(|i| {
152 let (min_bytes, max_bytes) = class_bounds(i);
153 SizeClass {
154 min_bytes,
155 max_bytes,
156 allocs: self.classes[i].allocs.load(Relaxed),
157 live_blocks: self.classes[i].live.load(Relaxed),
158 }
159 }),
160 }
161 }
162 }
163
164 static CLASSES: Histogram = Histogram::new();
165
166 pub(crate) fn record_alloc(size: usize) {
167 CLASSES.record_alloc(size);
168 }
169
170 pub(crate) fn record_free(size: usize) {
171 CLASSES.record_free(size);
172 }
173
174 pub(crate) fn record_realloc(old_size: usize, new_size: usize) {
175 CLASSES.record_realloc(old_size, new_size);
176 }
177
178 pub(crate) fn snapshot() -> Option<SizeClasses> {
179 Some(CLASSES.snapshot())
180 }
181}
182
183pub(crate) use imp::{record_alloc, record_free, record_realloc};
184
185pub fn size_classes() -> Option<SizeClasses> {
188 imp::snapshot()
189}
190
191#[cfg(test)]
192mod tests {
193 #[cfg(feature = "detail")]
194 use super::imp::Histogram;
195 use super::*;
196
197 #[test]
198 fn classes_partition_the_size_range_without_gaps_or_overlap() {
199 let (mut prev_min, mut prev_max) = class_bounds(0);
200 assert_eq!((prev_min, prev_max), (0, 0));
201 for class in 1..CLASS_COUNT {
202 let (min, max) = class_bounds(class);
203 assert_eq!(min, prev_max + 1, "class {class} does not follow the last");
204 assert!(max >= min);
205 (prev_min, prev_max) = (min, max);
206 }
207 assert!(prev_min > 0);
208 assert_eq!(prev_max, u64::MAX, "the top class must catch every size");
209 }
210
211 #[test]
212 fn a_size_lands_in_the_class_that_covers_it() {
213 for size in [0usize, 1, 2, 3, 4, 7, 8, 64, 1023, 1024, 1 << 20] {
214 let class = class_of(size);
215 let (min, max) = class_bounds(class);
216 assert!(
217 (min..=max).contains(&(size as u64)),
218 "{size} landed outside class {class} ({min}..={max})"
219 );
220 }
221 }
222
223 #[test]
224 fn an_enormous_size_lands_in_the_catch_all_class() {
225 assert_eq!(class_of(usize::MAX), CLASS_COUNT - 1);
226 assert_eq!(class_of(1 << 40), CLASS_COUNT - 1);
227 }
228
229 #[cfg(not(feature = "detail"))]
232 #[test]
233 fn the_histogram_is_absent_without_the_feature() {
234 record_alloc(64);
235 assert_eq!(size_classes(), None);
236 }
237
238 #[cfg(feature = "detail")]
239 #[test]
240 fn allocations_and_frees_move_their_class() {
241 const SIZE: usize = 1 << 17;
242 let class = class_of(SIZE);
243 let histogram = Histogram::new();
244
245 histogram.record_alloc(SIZE);
246 let during = histogram.snapshot().classes[class];
247 assert_eq!(during.allocs, 1);
248 assert_eq!(during.live_blocks, 1);
249
250 histogram.record_free(SIZE);
251 let after = histogram.snapshot().classes[class];
252 assert_eq!(after.live_blocks, 0);
253 assert_eq!(after.allocs, 1, "a free is not an alloc");
254 }
255
256 #[cfg(feature = "detail")]
259 #[test]
260 fn a_free_without_a_matching_alloc_leaves_the_class_empty() {
261 const SIZE: usize = 1 << 11;
262 let histogram = Histogram::new();
263
264 histogram.record_free(SIZE);
265
266 let class = histogram.snapshot().classes[class_of(SIZE)];
267 assert_eq!(class.live_blocks, 0);
268 assert_eq!(class.allocs, 0);
269 }
270
271 #[cfg(feature = "detail")]
274 #[test]
275 fn a_resize_moves_the_block_between_classes() {
276 const SMALL: usize = 1 << 13;
277 const LARGE: usize = 1 << 19;
278 let (small, large) = (class_of(SMALL), class_of(LARGE));
279 let histogram = Histogram::new();
280
281 histogram.record_alloc(SMALL);
282 histogram.record_realloc(SMALL, LARGE);
283 let after = histogram.snapshot();
284
285 assert_eq!(after.classes[small].live_blocks, 0);
286 assert_eq!(after.classes[large].live_blocks, 1);
287 assert_eq!(after.classes[large].allocs, 0, "a resize is not an alloc");
288 assert_eq!(after.classes[small].allocs, 1);
289 }
290
291 #[cfg(feature = "detail")]
293 #[test]
294 fn a_resize_within_a_class_leaves_the_counts_alone() {
295 const SMALL: usize = 1 << 13;
296 const LARGER: usize = (1 << 13) + 64;
297 let class = class_of(SMALL);
298 let histogram = Histogram::new();
299
300 histogram.record_alloc(SMALL);
301 histogram.record_realloc(SMALL, LARGER);
302
303 let after = histogram.snapshot().classes[class];
304 assert_eq!(after.live_blocks, 1);
305 assert_eq!(after.allocs, 1);
306 }
307
308 #[cfg(feature = "detail")]
309 #[test]
310 fn the_busiest_class_is_the_one_holding_the_most_live_blocks() {
311 const BUSY: usize = 1 << 9;
312 const QUIET: usize = 1 << 3;
313 let histogram = Histogram::new();
314
315 histogram.record_alloc(QUIET);
316 for _ in 0..3 {
317 histogram.record_alloc(BUSY);
318 }
319
320 let snapshot = histogram.snapshot();
321 let busiest = snapshot.busiest().expect("blocks are live");
322 assert_eq!(busiest.min_bytes, class_bounds(class_of(BUSY)).0);
323 assert_eq!(busiest.live_blocks, 3);
324 assert_eq!(snapshot.live_blocks(), 4);
325 }
326
327 #[cfg(feature = "detail")]
328 #[test]
329 fn an_empty_histogram_has_no_busiest_class() {
330 let histogram = Histogram::new();
331 assert_eq!(histogram.snapshot().busiest(), None);
332 assert_eq!(histogram.snapshot().live_blocks(), 0);
333 }
334
335 #[cfg(feature = "detail")]
340 #[test]
341 fn the_global_histogram_takes_what_the_allocator_records() {
342 const SIZE: usize = 1 << 17;
343 let class = class_of(SIZE);
344 let before = size_classes().expect("the feature is on").classes[class];
345
346 record_alloc(SIZE);
347 let after = size_classes().expect("the feature is on").classes[class];
348 assert!(after.allocs > before.allocs);
349
350 record_free(SIZE);
351 }
352
353 #[cfg(feature = "detail")]
357 #[test]
358 fn the_classes_are_readable_as_one_ascending_array() {
359 let snapshot = size_classes().expect("this build tracks size classes");
360 let classes = snapshot.classes();
361 assert_eq!(classes.len(), CLASS_COUNT);
362 for pair in classes.windows(2) {
363 assert!(
364 pair[0].max_bytes < pair[1].min_bytes || pair[0].max_bytes + 1 == pair[1].min_bytes,
365 "{:?} then {:?} do not meet",
366 pair[0],
367 pair[1]
368 );
369 }
370 assert_eq!(classes[CLASS_COUNT - 1].max_bytes, u64::MAX);
371 }
372}