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),
}
}
}
pub(crate) struct Histogram {
classes: [Class; CLASS_COUNT],
}
impl Histogram {
pub(crate) const fn new() -> Self {
Self {
classes: [const { Class::new() }; CLASS_COUNT],
}
}
pub(crate) fn record_alloc(&self, size: usize) {
let class = &self.classes[class_of(size)];
class.allocs.fetch_add(1, Relaxed);
class.live.fetch_add(1, Relaxed);
}
pub(crate) fn record_free(&self, size: usize) {
let live = &self.classes[class_of(size)].live;
let _ = live.fetch_update(Relaxed, Relaxed, |n| Some(n.saturating_sub(1)));
}
pub(crate) fn record_realloc(&self, old_size: usize, new_size: usize) {
let (old, new) = (class_of(old_size), class_of(new_size));
if old != new {
self.record_free(old_size);
self.classes[new].live.fetch_add(1, Relaxed);
}
}
pub(crate) fn snapshot(&self) -> SizeClasses {
SizeClasses {
classes: core::array::from_fn(|i| {
let (min_bytes, max_bytes) = class_bounds(i);
SizeClass {
min_bytes,
max_bytes,
allocs: self.classes[i].allocs.load(Relaxed),
live_blocks: self.classes[i].live.load(Relaxed),
}
}),
}
}
}
static CLASSES: Histogram = Histogram::new();
pub(crate) fn record_alloc(size: usize) {
CLASSES.record_alloc(size);
}
pub(crate) fn record_free(size: usize) {
CLASSES.record_free(size);
}
pub(crate) fn record_realloc(old_size: usize, new_size: usize) {
CLASSES.record_realloc(old_size, new_size);
}
pub(crate) fn snapshot() -> Option<SizeClasses> {
Some(CLASSES.snapshot())
}
}
pub(crate) use imp::{record_alloc, record_free, record_realloc};
pub fn size_classes() -> Option<SizeClasses> {
imp::snapshot()
}
#[cfg(test)]
mod tests {
#[cfg(feature = "detail")]
use super::imp::Histogram;
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 histogram = Histogram::new();
histogram.record_alloc(SIZE);
let during = histogram.snapshot().classes[class];
assert_eq!(during.allocs, 1);
assert_eq!(during.live_blocks, 1);
histogram.record_free(SIZE);
let after = histogram.snapshot().classes[class];
assert_eq!(after.live_blocks, 0);
assert_eq!(after.allocs, 1, "a free is not an alloc");
}
#[cfg(feature = "detail")]
#[test]
fn a_free_without_a_matching_alloc_leaves_the_class_empty() {
const SIZE: usize = 1 << 11;
let histogram = Histogram::new();
histogram.record_free(SIZE);
let class = histogram.snapshot().classes[class_of(SIZE)];
assert_eq!(class.live_blocks, 0);
assert_eq!(class.allocs, 0);
}
#[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 histogram = Histogram::new();
histogram.record_alloc(SMALL);
histogram.record_realloc(SMALL, LARGE);
let after = histogram.snapshot();
assert_eq!(after.classes[small].live_blocks, 0);
assert_eq!(after.classes[large].live_blocks, 1);
assert_eq!(after.classes[large].allocs, 0, "a resize is not an alloc");
assert_eq!(after.classes[small].allocs, 1);
}
#[cfg(feature = "detail")]
#[test]
fn a_resize_within_a_class_leaves_the_counts_alone() {
const SMALL: usize = 1 << 13;
const LARGER: usize = (1 << 13) + 64;
let class = class_of(SMALL);
let histogram = Histogram::new();
histogram.record_alloc(SMALL);
histogram.record_realloc(SMALL, LARGER);
let after = histogram.snapshot().classes[class];
assert_eq!(after.live_blocks, 1);
assert_eq!(after.allocs, 1);
}
#[cfg(feature = "detail")]
#[test]
fn the_busiest_class_is_the_one_holding_the_most_live_blocks() {
const BUSY: usize = 1 << 9;
const QUIET: usize = 1 << 3;
let histogram = Histogram::new();
histogram.record_alloc(QUIET);
for _ in 0..3 {
histogram.record_alloc(BUSY);
}
let snapshot = histogram.snapshot();
let busiest = snapshot.busiest().expect("blocks are live");
assert_eq!(busiest.min_bytes, class_bounds(class_of(BUSY)).0);
assert_eq!(busiest.live_blocks, 3);
assert_eq!(snapshot.live_blocks(), 4);
}
#[cfg(feature = "detail")]
#[test]
fn an_empty_histogram_has_no_busiest_class() {
let histogram = Histogram::new();
assert_eq!(histogram.snapshot().busiest(), None);
assert_eq!(histogram.snapshot().live_blocks(), 0);
}
#[cfg(feature = "detail")]
#[test]
fn the_global_histogram_takes_what_the_allocator_records() {
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 after = size_classes().expect("the feature is on").classes[class];
assert!(after.allocs > before.allocs);
record_free(SIZE);
}
#[cfg(feature = "detail")]
#[test]
fn the_classes_are_readable_as_one_ascending_array() {
let snapshot = size_classes().expect("this build tracks size classes");
let classes = snapshot.classes();
assert_eq!(classes.len(), CLASS_COUNT);
for pair in classes.windows(2) {
assert!(
pair[0].max_bytes < pair[1].min_bytes || pair[0].max_bytes + 1 == pair[1].min_bytes,
"{:?} then {:?} do not meet",
pair[0],
pair[1]
);
}
assert_eq!(classes[CLASS_COUNT - 1].max_bytes, u64::MAX);
}
}