use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering::Relaxed};
use lazily::Context;
const SMALL_MAX: usize = 1024;
static ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
static DEALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
static LIVE_BYTES: AtomicI64 = AtomicI64::new(0);
static LIVE_BLOCKS: AtomicI64 = AtomicI64::new(0);
static SMALL_HIST: [AtomicI64; SMALL_MAX + 1] = [const { AtomicI64::new(0) }; SMALL_MAX + 1];
static LARGE_HIST: [AtomicI64; 64] = [const { AtomicI64::new(0) }; 64];
static LARGE_BYTES: [AtomicI64; 64] = [const { AtomicI64::new(0) }; 64];
fn record_alloc(size: usize) {
ALLOC_CALLS.fetch_add(1, Relaxed);
LIVE_BYTES.fetch_add(size as i64, Relaxed);
LIVE_BLOCKS.fetch_add(1, Relaxed);
if size <= SMALL_MAX {
SMALL_HIST[size].fetch_add(1, Relaxed);
} else {
let bucket = usize::BITS as usize - 1 - size.leading_zeros() as usize;
LARGE_HIST[bucket].fetch_add(1, Relaxed);
LARGE_BYTES[bucket].fetch_add(size as i64, Relaxed);
}
}
fn record_dealloc(size: usize) {
DEALLOC_CALLS.fetch_add(1, Relaxed);
LIVE_BYTES.fetch_sub(size as i64, Relaxed);
LIVE_BLOCKS.fetch_sub(1, Relaxed);
if size <= SMALL_MAX {
SMALL_HIST[size].fetch_sub(1, Relaxed);
} else {
let bucket = usize::BITS as usize - 1 - size.leading_zeros() as usize;
LARGE_HIST[bucket].fetch_sub(1, Relaxed);
LARGE_BYTES[bucket].fetch_sub(size as i64, Relaxed);
}
}
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
record_alloc(layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
record_dealloc(layout.size());
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
if !ptr.is_null() {
record_alloc(layout.size());
}
ptr
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let out = unsafe { System.realloc(ptr, layout, new_size) };
if !out.is_null() {
record_dealloc(layout.size());
record_alloc(new_size);
}
out
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
#[derive(Clone, Copy, Default)]
struct Snapshot {
alloc_calls: u64,
dealloc_calls: u64,
live_bytes: i64,
live_blocks: i64,
}
fn snapshot() -> Snapshot {
Snapshot {
alloc_calls: ALLOC_CALLS.load(Relaxed),
dealloc_calls: DEALLOC_CALLS.load(Relaxed),
live_bytes: LIVE_BYTES.load(Relaxed),
live_blocks: LIVE_BLOCKS.load(Relaxed),
}
}
impl Snapshot {
fn since(self, base: Snapshot) -> Snapshot {
Snapshot {
alloc_calls: self.alloc_calls - base.alloc_calls,
dealloc_calls: self.dealloc_calls - base.dealloc_calls,
live_bytes: self.live_bytes - base.live_bytes,
live_blocks: self.live_blocks - base.live_blocks,
}
}
}
type Hist = [i64; SMALL_MAX + 1 + 128];
fn hist_capture() -> Hist {
let mut hist = [0i64; SMALL_MAX + 1 + 128];
for (size, cell) in hist.iter_mut().enumerate().take(SMALL_MAX + 1) {
*cell = SMALL_HIST[size].load(Relaxed);
}
for bucket in 0..64 {
hist[SMALL_MAX + 1 + bucket] = LARGE_HIST[bucket].load(Relaxed);
hist[SMALL_MAX + 1 + 64 + bucket] = LARGE_BYTES[bucket].load(Relaxed);
}
hist
}
struct HistRow {
blocks: i64,
bytes: i64,
exact_size: bool,
}
fn hist_rows(hist: &Hist, base: &Hist) -> Vec<HistRow> {
let mut rows: Vec<HistRow> = (0..=SMALL_MAX)
.filter_map(|size| {
let blocks = hist[size] - base[size];
(blocks != 0).then_some(HistRow {
blocks,
bytes: blocks * size as i64,
exact_size: true,
})
})
.collect();
rows.extend((11..64).filter_map(|bucket| {
let blocks = hist[SMALL_MAX + 1 + bucket] - base[SMALL_MAX + 1 + bucket];
let bytes = hist[SMALL_MAX + 1 + 64 + bucket] - base[SMALL_MAX + 1 + 64 + bucket];
(blocks != 0 || bytes != 0).then_some(HistRow {
blocks,
bytes,
exact_size: false,
})
}));
rows.sort_by_key(|row| std::cmp::Reverse(row.bytes));
rows
}
fn rss_kib() -> u64 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|status| {
status.lines().find_map(|line| {
line.strip_prefix("VmRSS:")?
.split_whitespace()
.next()?
.parse()
.ok()
})
})
.unwrap_or(0)
}
struct WidthReport {
width: usize,
ctx_only: Snapshot,
after_create: Snapshot,
after_register: Snapshot,
rss_bytes_each: f64,
phase_histogram_a: [Vec<HistRow>; 3],
}
fn sweep_width(width: usize) -> WidthReport {
let mut subscriber_a: Vec<_> = Vec::with_capacity(width);
let rss_before = rss_kib();
let base = snapshot();
let hist_base = hist_capture();
let ctx = Context::new();
let topic = ctx.cell(0u64);
let ctx_only = snapshot().since(base);
let hist_ctx = hist_capture();
subscriber_a
.extend((0..width).map(|i| ctx.computed(move |ctx| ctx.get_cell(&topic) + i as u64)));
let after_create = snapshot().since(base);
let hist_create = hist_capture();
for slot in &subscriber_a {
std::hint::black_box(ctx.get(slot));
}
let after_register = snapshot().since(base);
let hist_register = hist_capture();
let rss_bytes_each =
(rss_kib().saturating_sub(rss_before)) as f64 * 1024.0 / width.max(1) as f64;
let report = WidthReport {
width,
ctx_only,
after_create,
after_register,
rss_bytes_each,
phase_histogram_a: [
hist_rows(&hist_ctx, &hist_base),
hist_rows(&hist_create, &hist_ctx),
hist_rows(&hist_register, &hist_create),
],
};
drop(subscriber_a);
drop(ctx);
report
}
fn size_of_handle() -> usize {
std::mem::size_of::<lazily::SlotHandle<u64>>()
}
fn print_width_report(report: &WidthReport, previous: Option<&WidthReport>) {
let width = report.width as f64;
let total = report.after_register;
println!(
"\nwidth {} — {} live blocks, {} live bytes",
report.width, total.live_blocks, total.live_bytes,
);
println!(
" {:<28}{:>14}{:>14}{:>14}",
"phase", "live blocks", "live bytes", "bytes/sub"
);
let phases = [
("1 context + topic cell", report.ctx_only),
("2 +N computed (unread)", report.after_create),
("3 +N edges registered", report.after_register),
];
let mut previous_phase = Snapshot::default();
for (name, phase) in phases {
println!(
" {:<28}{:>14}{:>14}{:>14.2}",
name,
phase.live_blocks - previous_phase.live_blocks,
phase.live_bytes - previous_phase.live_bytes,
(phase.live_bytes - previous_phase.live_bytes) as f64 / width,
);
previous_phase = phase;
}
println!(
" {:<28}{:>14}{:>14}{:>14.2}",
"TOTAL",
total.live_blocks,
total.live_bytes,
total.live_bytes as f64 / width,
);
println!(
" counted {:.2} B/sub, {:.3} live blocks/sub | RSS {:.2} B/sub \
(RSS - counted = allocator metadata + page rounding)",
total.live_bytes as f64 / width,
total.live_blocks as f64 / width,
report.rss_bytes_each,
);
println!(" {:.2} alloc calls/sub", total.alloc_calls as f64 / width);
if let Some(previous) = previous {
let delta_width = (report.width - previous.width) as f64;
println!(
" marginal vs width {}: {:.2} B/sub, {:.3} blocks/sub \
(slope = what actually scales; total - slope*width = fixed)",
previous.width,
(total.live_bytes - previous.after_register.live_bytes) as f64 / delta_width,
(total.live_blocks - previous.after_register.live_blocks) as f64 / delta_width,
);
}
let phase_name_a = [
"1 context + topic cell",
"2 +N computed (unread)",
"3 +N edges registered",
];
for (name, histogram) in phase_name_a.iter().zip(report.phase_histogram_a.iter()) {
println!(" live blocks created by phase {name} (top 5):");
for row in histogram.iter().take(5) {
let mean = if row.blocks == 0 {
0
} else {
row.bytes / row.blocks
};
println!(
" {:>12} B {:<7} x {:>9} blocks = {:>14} B ({:>8.2} B/sub)",
mean,
if row.exact_size { "exact" } else { "mean" },
row.blocks,
row.bytes,
row.bytes as f64 / width,
);
}
}
}
fn sweep_heap_valued(width: usize) -> (i64, i64) {
let mut subscriber_a: Vec<_> = Vec::with_capacity(width);
let base = snapshot();
let ctx = Context::new();
let topic = ctx.cell(0u64);
subscriber_a.extend(
(0..width).map(|i| ctx.computed(move |ctx| format!("{}-{}", ctx.get_cell(&topic), i))),
);
for slot in &subscriber_a {
std::hint::black_box(ctx.get(slot));
}
let total = snapshot().since(base);
drop(subscriber_a);
drop(ctx);
(total.live_bytes, total.live_blocks)
}
fn main() {
println!("=== 1. static layout (size_of / align_of) ===\n");
#[cfg(audit_probe)]
{
println!(" {:<44}{:>8}{:>8}", "type", "size", "align");
for (name, size, align) in lazily::audit_probe::layout_rows() {
println!(" {name:<44}{size:>8}{align:>8}");
}
println!("\n SlotNode field offsets (gaps are padding):");
let mut offsets = lazily::audit_probe::slot_node_field_offsets();
offsets.sort_by_key(|(_, offset)| *offset);
for (name, offset) in &offsets {
println!(" {name:<20} @ {offset:>4} (cache line {})", offset / 64);
}
let mark_fields = ["dependents", "dirty", "force_recompute"];
let mut lines: Vec<usize> = offsets
.iter()
.filter(|(name, _)| mark_fields.contains(name))
.map(|(_, offset)| offset / 64)
.collect();
lines.sort_unstable();
lines.dedup();
let node_size = lazily::audit_probe::layout_rows()
.into_iter()
.find(|(name, _, _)| *name == "Option<super::Node>")
.map_or(0, |(_, size, _)| size);
println!();
println!(" invalidation (mark_frontier_locked) sets 2 bits of state per node.");
println!(" To reach them it strides {node_size} B of arena per node and touches");
println!(
" {} distinct 64 B cache line(s) {:?} inside each node.",
lines.len(),
lines,
);
}
#[cfg(not(audit_probe))]
println!(
" (skipped: rerun with RUSTFLAGS=\"--cfg audit_probe\" for the \
internal type layout)"
);
println!(
"\n SlotHandle<u64> (harness-side, not library memory): {} B",
size_of_handle()
);
println!("\n=== 2 + 3. counted allocations per subscriber ===");
let max_width: usize = std::env::var("LAZILY_AUDIT_MAX_WIDTH")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(1_000_000);
let ladder = [16usize, 32, 33, 64, 128, 1024, 4096, 65_536, 1_000_000];
std::hint::black_box(sweep_width(64));
let mut previous: Option<WidthReport> = None;
for width in ladder {
if width > max_width {
break;
}
let report = sweep_width(width);
print_width_report(&report, previous.as_ref());
previous = Some(report);
}
println!("\n=== 4. inline vs heap value storage ===\n");
let probe_width = 4096.min(max_width);
let inline = sweep_width(probe_width);
let (heap_bytes, heap_blocks) = sweep_heap_valued(probe_width);
let each = probe_width as f64;
println!(
" width {probe_width}: u64 (inline) {:>10} B, {:>8} blocks -> {:.2} B/sub, {:.3} blocks/sub",
inline.after_register.live_bytes,
inline.after_register.live_blocks,
inline.after_register.live_bytes as f64 / each,
inline.after_register.live_blocks as f64 / each,
);
println!(
" width {probe_width}: String (heap) {:>10} B, {:>8} blocks -> {:.2} B/sub, {:.3} blocks/sub",
heap_bytes,
heap_blocks,
heap_bytes as f64 / each,
heap_blocks as f64 / each,
);
println!(
" value-box cost when not inline-eligible: {:+.2} B/sub, {:+.3} blocks/sub",
(heap_bytes - inline.after_register.live_bytes) as f64 / each,
(heap_blocks - inline.after_register.live_blocks) as f64 / each,
);
let leaked = snapshot();
println!(
"\n=== leak check: {} live blocks / {} live bytes still held \
(harness Vecs and process statics only) ===",
leaked.live_blocks, leaked.live_bytes,
);
}