pub(crate) const CLASS_COUNT: usize = 33;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SizeClass {
pub min_bytes: u64,
pub max_bytes: u64,
pub allocs: u64,
pub live_blocks: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeClasses {
classes: [SizeClass; CLASS_COUNT],
}
impl SizeClasses {
pub fn classes(&self) -> &[SizeClass; CLASS_COUNT] {
&self.classes
}
pub fn busiest(&self) -> Option<SizeClass> {
self.classes
.iter()
.copied()
.filter(|c| c.live_blocks > 0)
.max_by_key(|c| c.live_blocks)
}
pub fn live_blocks(&self) -> u64 {
self.classes.iter().map(|c| c.live_blocks).sum()
}
}
#[cfg(any(feature = "detail", test))]
const fn class_of(size: usize) -> usize {
let bits = (usize::BITS - size.leading_zeros()) as usize;
if bits >= CLASS_COUNT {
CLASS_COUNT - 1
} else {
bits
}
}
#[cfg(any(feature = "detail", test))]
const fn class_bounds(class: usize) -> (u64, u64) {
match class {
0 => (0, 0),
c if c == CLASS_COUNT - 1 => (1 << (CLASS_COUNT - 2), u64::MAX),
c => (1 << (c - 1), (1 << c) - 1),
}
}
#[cfg(not(feature = "detail"))]
mod imp {
pub(crate) fn record_alloc(_size: usize) {}
pub(crate) fn record_free(_size: usize) {}
pub(crate) fn record_realloc(_old_size: usize, _new_size: usize) {}
pub(crate) fn snapshot() -> Option<super::SizeClasses> {
None
}
}
#[cfg(feature = "detail")]
mod imp {
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
use super::{CLASS_COUNT, SizeClass, SizeClasses, class_bounds, class_of};
struct Class {
allocs: AtomicU64,
live: AtomicU64,
}
impl Class {
const fn new() -> Self {
Self {
allocs: AtomicU64::new(0),
live: AtomicU64::new(0),
}
}
}
static CLASSES: [Class; CLASS_COUNT] = [const { Class::new() }; CLASS_COUNT];
pub(crate) fn record_alloc(size: usize) {
let class = &CLASSES[class_of(size)];
class.allocs.fetch_add(1, Relaxed);
class.live.fetch_add(1, Relaxed);
}
pub(crate) fn record_free(size: usize) {
let live = &CLASSES[class_of(size)].live;
let _ = live.fetch_update(Relaxed, Relaxed, |n| Some(n.saturating_sub(1)));
}
pub(crate) fn record_realloc(old_size: usize, new_size: usize) {
let (old, new) = (class_of(old_size), class_of(new_size));
if old != new {
record_free(old_size);
CLASSES[new].live.fetch_add(1, Relaxed);
}
}
pub(crate) fn snapshot() -> Option<SizeClasses> {
Some(SizeClasses {
classes: core::array::from_fn(|i| {
let (min_bytes, max_bytes) = class_bounds(i);
SizeClass {
min_bytes,
max_bytes,
allocs: CLASSES[i].allocs.load(Relaxed),
live_blocks: CLASSES[i].live.load(Relaxed),
}
}),
})
}
}
pub(crate) use imp::{record_alloc, record_free, record_realloc};
pub fn size_classes() -> Option<SizeClasses> {
imp::snapshot()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classes_partition_the_size_range_without_gaps_or_overlap() {
let (mut prev_min, mut prev_max) = class_bounds(0);
assert_eq!((prev_min, prev_max), (0, 0));
for class in 1..CLASS_COUNT {
let (min, max) = class_bounds(class);
assert_eq!(min, prev_max + 1, "class {class} does not follow the last");
assert!(max >= min);
(prev_min, prev_max) = (min, max);
}
assert!(prev_min > 0);
assert_eq!(prev_max, u64::MAX, "the top class must catch every size");
}
#[test]
fn a_size_lands_in_the_class_that_covers_it() {
for size in [0usize, 1, 2, 3, 4, 7, 8, 64, 1023, 1024, 1 << 20] {
let class = class_of(size);
let (min, max) = class_bounds(class);
assert!(
(min..=max).contains(&(size as u64)),
"{size} landed outside class {class} ({min}..={max})"
);
}
}
#[test]
fn an_enormous_size_lands_in_the_catch_all_class() {
assert_eq!(class_of(usize::MAX), CLASS_COUNT - 1);
assert_eq!(class_of(1 << 40), CLASS_COUNT - 1);
}
#[cfg(not(feature = "detail"))]
#[test]
fn the_histogram_is_absent_without_the_feature() {
record_alloc(64);
assert_eq!(size_classes(), None);
}
#[cfg(feature = "detail")]
#[test]
fn allocations_and_frees_move_their_class() {
const SIZE: usize = 1 << 17;
let class = class_of(SIZE);
let before = size_classes().expect("the feature is on").classes[class];
record_alloc(SIZE);
let during = size_classes().expect("the feature is on").classes[class];
assert_eq!(during.allocs, before.allocs + 1);
assert_eq!(during.live_blocks, before.live_blocks + 1);
record_free(SIZE);
let after = size_classes().expect("the feature is on").classes[class];
assert_eq!(after.live_blocks, before.live_blocks);
assert_eq!(after.allocs, before.allocs + 1, "a free is not an alloc");
}
#[cfg(feature = "detail")]
#[test]
fn a_resize_moves_the_block_between_classes() {
const SMALL: usize = 1 << 13;
const LARGE: usize = 1 << 19;
let (small, large) = (class_of(SMALL), class_of(LARGE));
let before = size_classes().expect("the feature is on");
record_alloc(SMALL);
record_realloc(SMALL, LARGE);
let after = size_classes().expect("the feature is on");
assert_eq!(
after.classes[small].live_blocks,
before.classes[small].live_blocks
);
assert_eq!(
after.classes[large].live_blocks,
before.classes[large].live_blocks + 1
);
assert_eq!(after.classes[large].allocs, before.classes[large].allocs);
record_free(LARGE);
}
#[cfg(feature = "detail")]
#[test]
fn the_busiest_class_is_the_one_holding_the_most_live_blocks() {
const SIZE: usize = 1 << 9;
let target = size_classes().expect("the feature is on").live_blocks() + 1;
for _ in 0..target {
record_alloc(SIZE);
}
let busiest = size_classes()
.expect("the feature is on")
.busiest()
.expect("blocks are live");
assert_eq!(busiest.min_bytes, class_bounds(class_of(SIZE)).0);
for _ in 0..target {
record_free(SIZE);
}
}
}