#[cfg(not(target_os = "macos"))]
fn main() {
eprintln!("size_class_histogram probes the macOS libmalloc class table; run it on macOS.");
}
#[cfg(target_os = "macos")]
fn main() {
macos::run();
}
#[cfg(target_os = "macos")]
mod macos {
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicIsize, AtomicU32, AtomicUsize, Ordering};
use lua_gc::GcBox;
use lua_types::closure::{LuaCClosure, LuaLClosure};
use lua_types::proto::LuaProto;
use lua_types::string::LuaString;
use lua_types::table::{LuaTable, TableInner, TableNode};
use lua_types::upval::UpVal;
use lua_types::userdata::LuaUserData;
use lua_types::value::{LuaThread, LuaValue};
use lua_vm::state::LuaState;
use omnilua::Lua;
extern "C" {
fn malloc_good_size(size: usize) -> usize;
}
fn good(size: usize) -> usize {
if size == 0 {
return 0;
}
unsafe { malloc_good_size(size) }
}
fn class_lower_edge(size: usize) -> usize {
let cap = good(size);
let (mut lo, mut hi) = (1usize, size);
while lo < hi {
let mid = (lo + hi) / 2;
if good(mid) < cap {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
}
fn prev_class_cap(size: usize) -> usize {
let edge = class_lower_edge(size);
if edge <= 1 {
0
} else {
good(edge - 1)
}
}
const GRANULARITY: usize = 1;
const NUM_BUCKETS: usize = 8192;
#[allow(clippy::declare_interior_mutable_const)]
const ZERO_U32: AtomicU32 = AtomicU32::new(0);
static LIVE_BYTES: AtomicIsize = AtomicIsize::new(0);
static PEAK_BYTES: AtomicUsize = AtomicUsize::new(0);
static LIVE: [AtomicU32; NUM_BUCKETS] = [ZERO_U32; NUM_BUCKETS];
static SNAP: [AtomicU32; NUM_BUCKETS] = [ZERO_U32; NUM_BUCKETS];
static LIVE_OVERFLOW: AtomicU32 = AtomicU32::new(0);
static SNAP_OVERFLOW: AtomicU32 = AtomicU32::new(0);
static SNAP_AT: AtomicUsize = AtomicUsize::new(0);
const SNAP_DELTA: usize = 4096;
static REPORTING: AtomicUsize = AtomicUsize::new(0);
fn bucket_of(size: usize) -> Option<usize> {
let b = size / GRANULARITY;
if b < NUM_BUCKETS {
Some(b)
} else {
None
}
}
fn record_alloc(size: usize) {
if REPORTING.load(Ordering::Relaxed) != 0 {
return;
}
match bucket_of(size) {
Some(b) => {
LIVE[b].fetch_add(1, Ordering::Relaxed);
}
None => {
LIVE_OVERFLOW.fetch_add(1, Ordering::Relaxed);
}
}
let now = (LIVE_BYTES.fetch_add(size as isize, Ordering::Relaxed) + size as isize) as usize;
if now > PEAK_BYTES.load(Ordering::Relaxed) {
PEAK_BYTES.store(now, Ordering::Relaxed);
if now >= SNAP_AT.load(Ordering::Relaxed) + SNAP_DELTA {
SNAP_AT.store(now, Ordering::Relaxed);
for i in 0..NUM_BUCKETS {
SNAP[i].store(LIVE[i].load(Ordering::Relaxed), Ordering::Relaxed);
}
SNAP_OVERFLOW.store(LIVE_OVERFLOW.load(Ordering::Relaxed), Ordering::Relaxed);
}
}
}
fn record_free(size: usize) {
if REPORTING.load(Ordering::Relaxed) != 0 {
return;
}
match bucket_of(size) {
Some(b) => {
LIVE[b].fetch_sub(1, Ordering::Relaxed);
}
None => {
LIVE_OVERFLOW.fetch_sub(1, Ordering::Relaxed);
}
}
LIVE_BYTES.fetch_sub(size as isize, Ordering::Relaxed);
}
struct HistAllocator;
unsafe impl GlobalAlloc for HistAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
record_alloc(layout.size());
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
record_free(layout.size());
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc_zeroed(layout) };
if !p.is_null() {
record_alloc(layout.size());
}
p
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new_size) };
if !p.is_null() {
record_free(layout.size());
record_alloc(new_size);
}
p
}
}
#[global_allocator]
static ALLOC: HistAllocator = HistAllocator;
struct TypeRow {
name: &'static str,
payload: usize,
boxed: usize,
}
fn row<T>(name: &'static str) -> TypeRow {
TypeRow {
name,
payload: std::mem::size_of::<T>(),
boxed: std::mem::size_of::<GcBox<T>>(),
}
}
fn snap_pop(size: usize) -> u32 {
match bucket_of(size) {
Some(b) => SNAP[b].load(Ordering::Relaxed),
None => 0,
}
}
pub(super) fn run() {
let path = std::env::args().nth(1).expect("usage: size_class_histogram <workload.lua>");
let source = std::fs::read(&path).expect("read workload");
{
let lua = Lua::new();
lua.load(&source).exec().expect("workload exec failed");
}
REPORTING.store(1, Ordering::Relaxed);
let rows = [
row::<LuaTable>("LuaTable"),
row::<LuaString>("LuaString"),
row::<UpVal>("UpVal"),
row::<LuaLClosure>("LuaLClosure"),
row::<LuaCClosure>("LuaCClosure"),
row::<LuaProto>("LuaProto"),
row::<LuaUserData>("LuaUserData"),
row::<LuaThread>("LuaThread"),
row::<LuaState>("LuaState"),
];
println!("# size_class_histogram");
println!("workload : {path}");
println!("peak live bytes (proxy for peak RSS) : {} bytes ({:.2} MiB)",
PEAK_BYTES.load(Ordering::Relaxed),
PEAK_BYTES.load(Ordering::Relaxed) as f64 / (1024.0 * 1024.0));
println!("pointer width : {} bits", usize::BITS);
println!();
println!("## macOS libmalloc size-class probes (malloc_good_size, authoritative)");
println!("{:>8} {:>10}", "request", "good_size");
let mut probe = 16usize;
let mut last = 0;
while probe <= 1024 {
let g = good(probe);
if g != last {
println!("{probe:>8} {g:>10}");
last = g;
}
probe += 8;
}
for &p in &[1536usize, 2048, 3072, 4096, 8192, 16384] {
println!("{p:>8} {:>10}", good(p));
}
println!();
println!("## GcBox<T> sizes vs the size-class table (GcHeader = {} B)",
std::mem::size_of::<lua_gc::GcHeader>());
println!(
"{:<14} {:>7} {:>7} {:>7} {:>9} {:>10} {:>10} {:>10}",
"type", "payld", "GcBox", "class", "prevcls", "cross(B)", "step(B)", "peakpop"
);
for r in &rows {
let cls = good(r.boxed);
let prev = prev_class_cap(r.boxed);
let cross = r.boxed.saturating_sub(prev);
let step = cls.saturating_sub(prev);
println!(
"{:<14} {:>7} {:>7} {:>7} {:>9} {:>10} {:>10} {:>10}",
r.name, r.payload, r.boxed, cls, prev, cross, step, snap_pop(r.boxed)
);
}
println!();
println!("legend: class=libmalloc class cap for GcBox; prevcls=next-smaller class cap;");
println!(" cross(B)=bytes to remove from GcBox to drop below prevcls (enter cheaper class);");
println!(" step(B)=RSS bytes/object reclaimed by crossing; peakpop=live count at peak.");
println!();
println!("## helper-type sizes (context)");
println!("LuaValue = {} B, TableNode = {} B, TableInner = {} B",
std::mem::size_of::<LuaValue>(),
std::mem::size_of::<TableNode>(),
std::mem::size_of::<TableInner>());
println!();
println!("## peak-moment live histogram (size buckets with >=8 live objects)");
println!("{:>8} {:>10} {:>12} {:>16}", "size", "good_size", "peak_count", "peak_bytes(good)");
let mut total_tracked_bytes = 0usize;
for b in 0..NUM_BUCKETS {
let c = SNAP[b].load(Ordering::Relaxed);
if c >= 8 {
let size = b * GRANULARITY;
let g = good(size.max(1));
let bytes = g * c as usize;
total_tracked_bytes += bytes;
println!("{size:>8} {g:>10} {c:>12} {bytes:>16}");
}
}
let of = SNAP_OVERFLOW.load(Ordering::Relaxed);
if of > 0 {
println!("(>{} B overflow bucket: {} live allocations)", NUM_BUCKETS * GRANULARITY, of);
}
println!();
println!("tracked good-size bytes across printed buckets: {total_tracked_bytes} \
({:.2} MiB)", total_tracked_bytes as f64 / (1024.0 * 1024.0));
}
}