use core::sync::atomic::{AtomicU64, Ordering};
use mnemosyne_core::constants::NUM_SIZE_CLASSES;
use mnemosyne_core::size_class::class_to_size;
use super::batch;
use super::snapshot::BinSnapshot;
#[inline]
fn sum_counter(arr: &[AtomicU64; NUM_SIZE_CLASSES]) -> u64 {
batch::flush_current_thread();
arr.iter()
.map(|c| c.load(Ordering::Relaxed))
.fold(0u64, u64::saturating_add)
}
#[inline(always)]
fn make_bin_snapshot(class: usize) -> BinSnapshot {
let alloc_count = batch::ALLOC_COUNT[class].load(Ordering::Relaxed);
let dealloc_count = batch::DEALLOC_COUNT[class].load(Ordering::Relaxed);
let block_size = class_to_size(class);
BinSnapshot {
alloc_count,
dealloc_count,
alloc_bytes: batch::allocation_bytes(alloc_count, block_size),
requested_bytes: batch::REQUESTED_BYTES[class].load(Ordering::Relaxed),
block_size,
live_estimate: alloc_count.saturating_sub(dealloc_count),
}
}
#[must_use]
pub fn bin_snapshot(class: usize) -> Option<BinSnapshot> {
if class >= NUM_SIZE_CLASSES {
return None;
}
batch::flush_current_thread();
Some(make_bin_snapshot(class))
}
#[must_use]
pub fn all_bin_snapshots() -> [BinSnapshot; NUM_SIZE_CLASSES] {
batch::flush_current_thread();
core::array::from_fn(make_bin_snapshot)
}
#[must_use]
pub fn hottest_class() -> Option<usize> {
let snapshots = all_bin_snapshots();
snapshots
.iter()
.enumerate()
.max_by_key(|(_, s)| s.alloc_count)
.and_then(|(idx, s)| if s.alloc_count > 0 { Some(idx) } else { None })
}
#[must_use]
pub fn total_live_bytes() -> u64 {
all_bin_snapshots()
.iter()
.map(|s| s.live_bytes())
.fold(0u64, u64::saturating_add)
}
#[must_use]
pub fn total_alloc_count() -> u64 {
sum_counter(&batch::ALLOC_COUNT)
}
pub fn reset_bin_stats() {
batch::RESET_GENERATION.fetch_add(1, Ordering::Relaxed);
batch::flush_current_thread();
for class in 0..NUM_SIZE_CLASSES {
batch::ALLOC_COUNT[class].store(0, Ordering::Relaxed);
batch::DEALLOC_COUNT[class].store(0, Ordering::Relaxed);
batch::REQUESTED_BYTES[class].store(0, Ordering::Relaxed);
}
}
#[inline]
pub fn flush_tls_stats() {
batch::flush_current_thread();
}
#[must_use]
pub fn summary_line() -> std::string::String {
let total_allocs = total_alloc_count();
let live = total_live_bytes();
let int_frag = total_internal_fragmentation();
match hottest_class() {
Some(cls) => std::format!(
"allocs={total_allocs} live_bytes={live} int_frag={int_frag:.1}% hottest_class={cls}({}b)",
class_to_size(cls)
),
None => std::format!(
"allocs={total_allocs} live_bytes={live} int_frag={int_frag:.1}% hottest_class=none"
),
}
}
#[must_use]
pub fn total_requested_bytes() -> u64 {
sum_counter(&batch::REQUESTED_BYTES)
}
#[must_use]
pub fn total_internal_fragmentation() -> f64 {
let snapshots = all_bin_snapshots();
let alloc: u64 = snapshots
.iter()
.map(|s| s.alloc_bytes)
.fold(0, u64::saturating_add);
let requested: u64 = snapshots
.iter()
.map(|s| s.requested_bytes)
.fold(0, u64::saturating_add);
if alloc == 0 || requested == 0 {
return 0.0;
}
let waste = alloc.saturating_sub(requested);
(waste as f64 / alloc as f64).min(1.0)
}
#[inline]
#[must_use]
pub fn reset_generation_count() -> u32 {
batch::RESET_GENERATION.load(Ordering::Relaxed)
}
#[must_use]
pub fn alloc_distribution() -> [f64; NUM_SIZE_CLASSES] {
let snapshots = all_bin_snapshots();
let total: u64 = snapshots
.iter()
.map(|s| s.alloc_count)
.fold(0, u64::saturating_add);
if total == 0 {
return [0.0; NUM_SIZE_CLASSES];
}
let mut dist = [0.0f64; NUM_SIZE_CLASSES];
for (i, s) in snapshots.iter().enumerate() {
dist[i] = s.alloc_count as f64 / total as f64;
}
dist
}
#[cfg(test)]
mod tests {
use core::sync::atomic::AtomicU64;
use std::sync::{Arc, Barrier};
use super::batch::{FLUSH_BATCH, PendingCount, RESET_GENERATION, record_alloc_with_size};
use super::{
NUM_SIZE_CLASSES, all_bin_snapshots, bin_snapshot, class_to_size, flush_tls_stats,
reset_bin_stats, reset_generation_count,
};
#[test]
fn pending_counts_flush_without_dropping_observations() {
let global = [const { AtomicU64::new(0) }; NUM_SIZE_CLASSES];
let mut pending = PendingCount::new();
for _ in 0..FLUSH_BATCH {
pending.record(3, &global);
}
assert_eq!(
global[3].load(core::sync::atomic::Ordering::Relaxed),
FLUSH_BATCH as u64
);
assert_eq!(pending.count, 0);
}
#[test]
fn generation_counter_discards_stale_batches() {
let global = [const { AtomicU64::new(0) }; NUM_SIZE_CLASSES];
let mut pending = PendingCount::new();
pending.record(2, &global);
RESET_GENERATION.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
pending.flush(&global);
assert_eq!(
global[2].load(core::sync::atomic::Ordering::Relaxed),
0,
"stale batch must not flush after generation advance"
);
RESET_GENERATION.fetch_sub(1, core::sync::atomic::Ordering::Relaxed);
}
#[test]
fn reset_bin_stats_monotonically_advances_generation() {
let gen_before = reset_generation_count();
reset_bin_stats();
let gen_after = reset_generation_count();
assert!(
gen_after > gen_before,
"reset_bin_stats must advance the generation counter: \
before={gen_before} after={gen_after}"
);
for (class, snap) in all_bin_snapshots().iter().enumerate() {
assert_eq!(
snap.alloc_count, 0,
"class {class} alloc_count must be zero immediately after reset"
);
assert_eq!(
snap.dealloc_count, 0,
"class {class} dealloc_count must be zero immediately after reset"
);
}
}
#[test]
fn derived_allocation_bytes_match_the_class_stride() {
for snapshot in all_bin_snapshots() {
assert_eq!(
snapshot.alloc_bytes,
snapshot
.alloc_count
.saturating_mul(snapshot.block_size as u64),
"allocation bytes must be derived from the immutable class stride"
);
}
}
#[test]
fn snapshots_preserve_the_public_range_contract() {
assert!(bin_snapshot(NUM_SIZE_CLASSES).is_none());
}
#[test]
fn reset_excludes_a_batch_pending_on_another_thread() {
const CLASS: usize = 5;
const PRE_RESET: u32 = FLUSH_BATCH / 2;
const POST_RESET: u32 = FLUSH_BATCH / 4;
let barrier = Arc::new(Barrier::new(2));
let worker = {
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
for _ in 0..PRE_RESET {
record_alloc_with_size(CLASS, class_to_size(CLASS));
}
barrier.wait(); barrier.wait(); flush_tls_stats();
for _ in 0..POST_RESET {
record_alloc_with_size(CLASS, class_to_size(CLASS));
}
flush_tls_stats();
})
};
barrier.wait();
reset_bin_stats();
barrier.wait();
worker.join().expect("worker thread panicked");
let snapshot = bin_snapshot(CLASS).expect("invariant: CLASS < NUM_SIZE_CLASSES");
assert_eq!(
snapshot.alloc_count,
u64::from(POST_RESET),
"post-reset total must exclude the {PRE_RESET} allocations pending on the worker"
);
assert_eq!(
snapshot.requested_bytes,
u64::from(POST_RESET) * class_to_size(CLASS) as u64,
);
}
}