use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicIsize, Ordering};
use std::sync::Arc;
use regolith::{Db, Options, Statistics, Ticker};
static LIVE: AtomicIsize = AtomicIsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
let p = System.alloc(layout);
if !p.is_null() {
LIVE.fetch_add(layout.size() as isize, Ordering::Relaxed);
}
p
}
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe {
LIVE.fetch_sub(layout.size() as isize, Ordering::Relaxed);
System.dealloc(ptr, layout)
}
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
unsafe {
let p = System.realloc(ptr, layout, new_size);
if !p.is_null() {
LIVE.fetch_add(
new_size as isize - layout.size() as isize,
Ordering::Relaxed,
);
}
p
}
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
unsafe {
let p = System.alloc_zeroed(layout);
if !p.is_null() {
LIVE.fetch_add(layout.size() as isize, Ordering::Relaxed);
}
p
}
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
const KEYS: u32 = 60_000;
const DIR_ENV: &str = "REGOLITH_ADV_OVERHEAD_DIR";
const CACHE_ENV: &str = "REGOLITH_ADV_OVERHEAD_CACHE";
const BS_ENV: &str = "REGOLITH_ADV_OVERHEAD_BS";
const ROUNDS_ENV: &str = "REGOLITH_ADV_OVERHEAD_ROUNDS";
fn key(i: u32) -> Vec<u8> {
format!("k{i:07}").into_bytes()
}
#[test]
fn adv_overhead_child() {
let (Ok(dir), Ok(cache)) = (std::env::var(DIR_ENV), std::env::var(CACHE_ENV)) else {
return;
};
let cache_bytes: usize = cache.parse().expect("cache size");
let block_size: usize = std::env::var(BS_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1024);
let stats = Arc::new(Statistics::new());
let db = Db::open(
&dir,
Options {
block_cache_size: cache_bytes,
block_cache_num_shard_bits: 0,
block_size,
statistics: Some(Arc::clone(&stats)),
..Options::default()
},
)
.unwrap();
let rounds: u32 = std::env::var(ROUNDS_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2);
let heap_open = LIVE.load(Ordering::Relaxed);
for _ in 0..rounds {
for i in 0..KEYS {
assert!(db.get(&key(i)).unwrap().is_some());
}
}
let heap_warm = LIVE.load(Ordering::Relaxed);
let usage = db.get_int_property("regolith.block-cache-usage").unwrap();
let capacity = db
.get_int_property("regolith.block-cache-capacity")
.unwrap();
let adds = stats.get_ticker(Ticker::BlockCacheAdd);
let hits = stats.get_ticker(Ticker::BlockCacheHit);
let misses = stats.get_ticker(Ticker::BlockCacheMiss);
println!(
"ADVRESULT cache={cache_bytes} heap={} usage={usage} capacity={capacity} \
adds={adds} hits={hits} misses={misses}",
heap_warm - heap_open
);
drop(db);
}
#[test]
fn a_cached_entry_costs_no_more_heap_than_it_is_charged() {
if std::env::var(DIR_ENV).is_ok() {
return;
}
let prepare = |block_size: usize| -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
let db = Db::open(
dir.path(),
Options {
block_cache_size: 0,
block_size,
write_buffer_size: 4 * 1024 * 1024,
..Options::default()
},
)
.unwrap();
for i in 0..KEYS {
db.put(&key(i), &[(i % 251) as u8; 96]).unwrap();
}
db.compact_range(None, None).unwrap();
db.close().unwrap();
dir
};
let dir = prepare(1024);
let small_dir = prepare(256);
let exe = std::env::current_exe().expect("test binary");
struct Row {
heap: i64,
usage: i64,
capacity: i64,
adds: i64,
}
let run_rounds = |data: &std::path::Path, cache_bytes: usize, rounds: u32| -> Row {
let out = std::process::Command::new(&exe)
.args(["--exact", "adv_overhead_child", "--nocapture"])
.env(DIR_ENV, data)
.env(CACHE_ENV, cache_bytes.to_string())
.env(ROUNDS_ENV, rounds.to_string())
.output()
.expect("spawn child");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"child with cache={cache_bytes} failed:\n{stdout}{}",
String::from_utf8_lossy(&out.stderr)
);
let line = stdout
.lines()
.find(|l| l.starts_with("ADVRESULT"))
.unwrap_or_else(|| panic!("no result from child:\n{stdout}"))
.to_string();
println!("{line}");
let field = |name: &str| -> i64 {
line.split_whitespace()
.find_map(|f| f.strip_prefix(name))
.and_then(|v| v.parse().ok())
.unwrap_or_else(|| panic!("missing {name} in {line}"))
};
Row {
heap: field("heap="),
usage: field("usage="),
capacity: field("capacity="),
adds: field("adds="),
}
};
let run_in = |data: &std::path::Path, cache_bytes: usize| run_rounds(data, cache_bytes, 2);
let run = |cache_bytes: usize| run_in(dir.path(), cache_bytes);
let off = run(0);
assert_eq!(off.usage, 0, "a zero budget cached bytes");
assert!(
off.heap.abs() < 64 * 1024,
"a disabled cache grew the heap by {} bytes over a read workload",
off.heap
);
let roomy = run(64 * 1024 * 1024);
assert!(
roomy.adds > 0 && roomy.usage > 0,
"the workload never filled the cache"
);
let roomy_real = roomy.heap - off.heap;
let roomy_excess = roomy_real - roomy.usage;
println!(
"ADVOVERHEAD roomy real={roomy_real} charged={} excess={roomy_excess} \
entries={} excess_per_entry={:.2} charged_per_entry={:.1}",
roomy.usage,
roomy.adds,
roomy_excess as f64 / roomy.adds as f64,
roomy.usage as f64 / roomy.adds as f64
);
let tight = run(2 * 1024 * 1024);
let tight_real = tight.heap - off.heap;
println!(
"ADVOVERHEAD saturated real={tight_real} charged={} capacity={} adds={} \
over_capacity={} ratio={:.4}",
tight.usage,
tight.capacity,
tight.adds,
tight_real - tight.capacity,
tight_real as f64 / tight.capacity as f64
);
let tiny_off = run_in(small_dir.path(), 0);
let tiny = run_in(small_dir.path(), 2 * 1024 * 1024);
let tiny_real = tiny.heap - tiny_off.heap;
println!(
"ADVOVERHEAD small_blocks real={tiny_real} charged={} capacity={} adds={} \
charged_per_entry={:.1} over_capacity={} ratio={:.4}",
tiny.usage,
tiny.capacity,
tiny.adds,
tiny.usage as f64 / tiny.adds.max(1) as f64,
tiny_real - tiny.capacity,
tiny_real as f64 / tiny.capacity as f64
);
assert!(
roomy_real <= roomy.usage + roomy.adds * 16,
"an unevicted entry costs {} heap bytes more than the charge it is charged",
roomy_excess as f64 / roomy.adds as f64
);
for (label, real, capacity, ceiling) in [
("1 KiB blocks", tight_real, tight.capacity, 18),
("256 B blocks", tiny_real, tiny.capacity, 24),
] {
assert!(
real <= capacity * ceiling / 10,
"a saturated cache with {label} holds {real} heap bytes against a {capacity}-byte budget, past the {:.1}x this configuration is allowed",
ceiling as f64 / 10.0
);
}
let churned = run_rounds(small_dir.path(), 2 * 1024 * 1024, 6);
let churned_real = churned.heap - tiny_off.heap;
println!(
"ADVOVERHEAD churn_flat rounds=2 real={tiny_real} adds={} | rounds=6 real={churned_real} adds={} ratio={:.4}",
tiny.adds,
churned.adds,
churned_real as f64 / tiny_real as f64
);
assert!(
churned.adds > tiny.adds,
"the longer run cached no more entries than the short one, so it is not more churn: {} against {}",
churned.adds,
tiny.adds
);
let extra = churned_real.saturating_sub(tiny_real);
let per_add = extra as f64 / (churned.adds - tiny.adds).max(1) as f64;
println!("ADVOVERHEAD retained_per_insert={per_add:.1} bytes");
assert!(
per_add <= 40.0,
"the cache retains {per_add:.1} bytes per insert, so resident memory grows with churn \
rather than with the working set. Undrained this measures 144 bytes per insert, and a \
working drain measures under 15 even on a loaded machine"
);
}