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 static CLASSES: [Class; CLASS_COUNT] = [const { Class::new() }; CLASS_COUNT];
114
115 pub(crate) fn record_alloc(size: usize) {
116 let class = &CLASSES[class_of(size)];
117 class.allocs.fetch_add(1, Relaxed);
118 class.live.fetch_add(1, Relaxed);
119 }
120
121 pub(crate) fn record_free(size: usize) {
122 let live = &CLASSES[class_of(size)].live;
123 let _ = live.fetch_update(Relaxed, Relaxed, |n| Some(n.saturating_sub(1)));
124 }
125
126 pub(crate) fn record_realloc(old_size: usize, new_size: usize) {
129 let (old, new) = (class_of(old_size), class_of(new_size));
130 if old != new {
131 record_free(old_size);
132 CLASSES[new].live.fetch_add(1, Relaxed);
133 }
134 }
135
136 pub(crate) fn snapshot() -> Option<SizeClasses> {
137 Some(SizeClasses {
138 classes: core::array::from_fn(|i| {
139 let (min_bytes, max_bytes) = class_bounds(i);
140 SizeClass {
141 min_bytes,
142 max_bytes,
143 allocs: CLASSES[i].allocs.load(Relaxed),
144 live_blocks: CLASSES[i].live.load(Relaxed),
145 }
146 }),
147 })
148 }
149}
150
151pub(crate) use imp::{record_alloc, record_free, record_realloc};
152
153pub fn size_classes() -> Option<SizeClasses> {
156 imp::snapshot()
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn classes_partition_the_size_range_without_gaps_or_overlap() {
165 let (mut prev_min, mut prev_max) = class_bounds(0);
166 assert_eq!((prev_min, prev_max), (0, 0));
167 for class in 1..CLASS_COUNT {
168 let (min, max) = class_bounds(class);
169 assert_eq!(min, prev_max + 1, "class {class} does not follow the last");
170 assert!(max >= min);
171 (prev_min, prev_max) = (min, max);
172 }
173 assert!(prev_min > 0);
174 assert_eq!(prev_max, u64::MAX, "the top class must catch every size");
175 }
176
177 #[test]
178 fn a_size_lands_in_the_class_that_covers_it() {
179 for size in [0usize, 1, 2, 3, 4, 7, 8, 64, 1023, 1024, 1 << 20] {
180 let class = class_of(size);
181 let (min, max) = class_bounds(class);
182 assert!(
183 (min..=max).contains(&(size as u64)),
184 "{size} landed outside class {class} ({min}..={max})"
185 );
186 }
187 }
188
189 #[test]
190 fn an_enormous_size_lands_in_the_catch_all_class() {
191 assert_eq!(class_of(usize::MAX), CLASS_COUNT - 1);
192 assert_eq!(class_of(1 << 40), CLASS_COUNT - 1);
193 }
194
195 #[cfg(not(feature = "detail"))]
198 #[test]
199 fn the_histogram_is_absent_without_the_feature() {
200 record_alloc(64);
201 assert_eq!(size_classes(), None);
202 }
203
204 #[cfg(feature = "detail")]
206 #[test]
207 fn allocations_and_frees_move_their_class() {
208 const SIZE: usize = 1 << 17;
209 let class = class_of(SIZE);
210 let before = size_classes().expect("the feature is on").classes[class];
211
212 record_alloc(SIZE);
213 let during = size_classes().expect("the feature is on").classes[class];
214 assert_eq!(during.allocs, before.allocs + 1);
215 assert_eq!(during.live_blocks, before.live_blocks + 1);
216
217 record_free(SIZE);
218 let after = size_classes().expect("the feature is on").classes[class];
219 assert_eq!(after.live_blocks, before.live_blocks);
220 assert_eq!(after.allocs, before.allocs + 1, "a free is not an alloc");
221 }
222
223 #[cfg(feature = "detail")]
226 #[test]
227 fn a_resize_moves_the_block_between_classes() {
228 const SMALL: usize = 1 << 13;
229 const LARGE: usize = 1 << 19;
230 let (small, large) = (class_of(SMALL), class_of(LARGE));
231 let before = size_classes().expect("the feature is on");
232
233 record_alloc(SMALL);
234 record_realloc(SMALL, LARGE);
235 let after = size_classes().expect("the feature is on");
236
237 assert_eq!(
238 after.classes[small].live_blocks,
239 before.classes[small].live_blocks
240 );
241 assert_eq!(
242 after.classes[large].live_blocks,
243 before.classes[large].live_blocks + 1
244 );
245 assert_eq!(after.classes[large].allocs, before.classes[large].allocs);
246
247 record_free(LARGE);
248 }
249
250 #[cfg(feature = "detail")]
251 #[test]
252 fn the_busiest_class_is_the_one_holding_the_most_live_blocks() {
253 const SIZE: usize = 1 << 9;
254 let target = size_classes().expect("the feature is on").live_blocks() + 1;
256 for _ in 0..target {
257 record_alloc(SIZE);
258 }
259
260 let busiest = size_classes()
261 .expect("the feature is on")
262 .busiest()
263 .expect("blocks are live");
264 assert_eq!(busiest.min_bytes, class_bounds(class_of(SIZE)).0);
265
266 for _ in 0..target {
267 record_free(SIZE);
268 }
269 }
270}