1pub(crate) const CLASS_COUNT: usize = 33;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub struct SizeClass {
28 pub min_bytes: u64,
30 pub max_bytes: u64,
32 pub allocs: u64,
34 pub live_blocks: u64,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct SizeClasses {
41 classes: [SizeClass; CLASS_COUNT],
42}
43
44impl SizeClasses {
45 pub fn classes(&self) -> &[SizeClass; CLASS_COUNT] {
46 &self.classes
47 }
48
49 pub fn busiest(&self) -> Option<SizeClass> {
52 self.classes
53 .iter()
54 .copied()
55 .filter(|c| c.live_blocks > 0)
56 .max_by_key(|c| c.live_blocks)
57 }
58
59 pub fn live_blocks(&self) -> u64 {
60 self.classes.iter().map(|c| c.live_blocks).sum()
61 }
62}
63
64#[cfg(any(feature = "detail", test))]
66const fn class_of(size: usize) -> usize {
67 let bits = (usize::BITS - size.leading_zeros()) as usize;
68 if bits >= CLASS_COUNT {
69 CLASS_COUNT - 1
70 } else {
71 bits
72 }
73}
74
75#[cfg(any(feature = "detail", test))]
77const fn class_bounds(class: usize) -> (u64, u64) {
78 match class {
79 0 => (0, 0),
80 c if c == CLASS_COUNT - 1 => (1 << (CLASS_COUNT - 2), u64::MAX),
81 c => (1 << (c - 1), (1 << c) - 1),
82 }
83}
84
85#[cfg(not(feature = "detail"))]
86mod imp {
87 pub(crate) fn record_alloc(_size: usize) {}
88 pub(crate) fn record_free(_size: usize) {}
89 pub(crate) fn record_realloc(_old_size: usize, _new_size: usize) {}
90 pub(crate) fn snapshot() -> Option<super::SizeClasses> {
91 None
92 }
93}
94
95#[cfg(feature = "detail")]
96mod imp {
97 use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
98
99 use super::{CLASS_COUNT, SizeClass, SizeClasses, class_bounds, class_of};
100
101 struct Class {
102 allocs: AtomicU64,
103 live: AtomicU64,
104 }
105
106 impl Class {
107 const fn new() -> Self {
108 Self {
109 allocs: AtomicU64::new(0),
110 live: AtomicU64::new(0),
111 }
112 }
113 }
114
115 static CLASSES: [Class; CLASS_COUNT] = [const { Class::new() }; CLASS_COUNT];
116
117 pub(crate) fn record_alloc(size: usize) {
118 let class = &CLASSES[class_of(size)];
119 class.allocs.fetch_add(1, Relaxed);
120 class.live.fetch_add(1, Relaxed);
121 }
122
123 pub(crate) fn record_free(size: usize) {
124 let live = &CLASSES[class_of(size)].live;
125 let _ = live.fetch_update(Relaxed, Relaxed, |n| Some(n.saturating_sub(1)));
126 }
127
128 pub(crate) fn record_realloc(old_size: usize, new_size: usize) {
131 let (old, new) = (class_of(old_size), class_of(new_size));
132 if old != new {
133 record_free(old_size);
134 CLASSES[new].live.fetch_add(1, Relaxed);
135 }
136 }
137
138 pub(crate) fn snapshot() -> Option<SizeClasses> {
139 Some(SizeClasses {
140 classes: core::array::from_fn(|i| {
141 let (min_bytes, max_bytes) = class_bounds(i);
142 SizeClass {
143 min_bytes,
144 max_bytes,
145 allocs: CLASSES[i].allocs.load(Relaxed),
146 live_blocks: CLASSES[i].live.load(Relaxed),
147 }
148 }),
149 })
150 }
151}
152
153pub(crate) use imp::{record_alloc, record_free, record_realloc};
154
155pub fn size_classes() -> Option<SizeClasses> {
158 imp::snapshot()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn classes_partition_the_size_range_without_gaps_or_overlap() {
167 let (mut prev_min, mut prev_max) = class_bounds(0);
168 assert_eq!((prev_min, prev_max), (0, 0));
169 for class in 1..CLASS_COUNT {
170 let (min, max) = class_bounds(class);
171 assert_eq!(min, prev_max + 1, "class {class} does not follow the last");
172 assert!(max >= min);
173 (prev_min, prev_max) = (min, max);
174 }
175 assert!(prev_min > 0);
176 assert_eq!(prev_max, u64::MAX, "the top class must catch every size");
177 }
178
179 #[test]
180 fn a_size_lands_in_the_class_that_covers_it() {
181 for size in [0usize, 1, 2, 3, 4, 7, 8, 64, 1023, 1024, 1 << 20] {
182 let class = class_of(size);
183 let (min, max) = class_bounds(class);
184 assert!(
185 (min..=max).contains(&(size as u64)),
186 "{size} landed outside class {class} ({min}..={max})"
187 );
188 }
189 }
190
191 #[test]
192 fn an_enormous_size_lands_in_the_catch_all_class() {
193 assert_eq!(class_of(usize::MAX), CLASS_COUNT - 1);
194 assert_eq!(class_of(1 << 40), CLASS_COUNT - 1);
195 }
196
197 #[cfg(not(feature = "detail"))]
200 #[test]
201 fn the_histogram_is_absent_without_the_feature() {
202 record_alloc(64);
203 assert_eq!(size_classes(), None);
204 }
205
206 #[cfg(feature = "detail")]
208 #[test]
209 fn allocations_and_frees_move_their_class() {
210 const SIZE: usize = 1 << 17;
211 let class = class_of(SIZE);
212 let before = size_classes().expect("the feature is on").classes[class];
213
214 record_alloc(SIZE);
215 let during = size_classes().expect("the feature is on").classes[class];
216 assert_eq!(during.allocs, before.allocs + 1);
217 assert_eq!(during.live_blocks, before.live_blocks + 1);
218
219 record_free(SIZE);
220 let after = size_classes().expect("the feature is on").classes[class];
221 assert_eq!(after.live_blocks, before.live_blocks);
222 assert_eq!(after.allocs, before.allocs + 1, "a free is not an alloc");
223 }
224
225 #[cfg(feature = "detail")]
228 #[test]
229 fn a_resize_moves_the_block_between_classes() {
230 const SMALL: usize = 1 << 13;
231 const LARGE: usize = 1 << 19;
232 let (small, large) = (class_of(SMALL), class_of(LARGE));
233 let before = size_classes().expect("the feature is on");
234
235 record_alloc(SMALL);
236 record_realloc(SMALL, LARGE);
237 let after = size_classes().expect("the feature is on");
238
239 assert_eq!(
240 after.classes[small].live_blocks,
241 before.classes[small].live_blocks
242 );
243 assert_eq!(
244 after.classes[large].live_blocks,
245 before.classes[large].live_blocks + 1
246 );
247 assert_eq!(after.classes[large].allocs, before.classes[large].allocs);
248
249 record_free(LARGE);
250 }
251
252 #[cfg(feature = "detail")]
253 #[test]
254 fn the_busiest_class_is_the_one_holding_the_most_live_blocks() {
255 const SIZE: usize = 1 << 9;
256 let target = size_classes().expect("the feature is on").live_blocks() + 1;
258 for _ in 0..target {
259 record_alloc(SIZE);
260 }
261
262 let busiest = size_classes()
263 .expect("the feature is on")
264 .busiest()
265 .expect("blocks are live");
266 assert_eq!(busiest.min_bytes, class_bounds(class_of(SIZE)).0);
267
268 for _ in 0..target {
269 record_free(SIZE);
270 }
271 }
272}