mod common;
use std::time::{Duration, Instant};
use regolith::{Options, WriteBatch};
const BATCH_SIZES: [usize; 4] = [1, 8, 32, 256];
const VALUE_BYTES: usize = 100;
const BULK_VALUE_BYTES: usize = 1024;
const BULK_BATCH_OPS: usize = 256;
const WARMUP_BATCHES: u64 = 8;
const UNSTABLE_SPREAD: f64 = 0.5;
fn env_u64(name: &str, default: u64) -> u64 {
match std::env::var(name) {
Ok(v) => v
.parse()
.unwrap_or_else(|_| panic!("{name}: expected an integer, got {v:?}")),
Err(_) => default,
}
}
fn opts() -> Options {
Options {
write_buffer_size: 64 * 1024 * 1024,
block_cache_size: 8 * 1024 * 1024,
block_cache_num_shard_bits: 0,
..common::default_opts()
}
}
fn batch_rep(batch_ops: usize, dur: Duration) -> f64 {
let (tmp, db) = common::open("batch", opts());
let mut rng = common::Rng::new(0xBA7C_0001 ^ batch_ops as u64);
let value = common::rand_value(&mut rng, VALUE_BYTES);
let mut next: u64 = 0;
let fill = |next: &mut u64| {
let mut wb = WriteBatch::new();
for _ in 0..batch_ops {
wb.put(&common::key(*next), &value);
*next += 1;
}
wb
};
for _ in 0..WARMUP_BATCHES {
db.write(fill(&mut next)).expect("warmup batch");
}
let start = Instant::now();
let deadline = start + dur;
let mut batches: u64 = 0;
while Instant::now() < deadline {
db.write(fill(&mut next)).expect("batch write");
batches += 1;
}
let elapsed = start.elapsed().as_secs_f64();
db.close().expect("close db");
drop(db);
drop(tmp);
(batches * batch_ops as u64) as f64 / elapsed
}
fn bulk_rep(payload_mib: u64) -> f64 {
let (tmp, db) = common::open("batch-bulk", opts());
let mut rng = common::Rng::new(0xB01C_0001);
let value = common::rand_value(&mut rng, BULK_VALUE_BYTES);
let per_op = (common::key(0).len() + BULK_VALUE_BYTES) as u64;
let target = payload_mib * 1024 * 1024;
let total_ops = target / per_op;
let start = Instant::now();
let mut next: u64 = 0;
while next < total_ops {
let mut wb = WriteBatch::new();
for _ in 0..BULK_BATCH_OPS.min((total_ops - next) as usize) {
wb.put(&common::key(next), &value);
next += 1;
}
db.write(wb).expect("bulk batch write");
}
db.close().expect("close db");
let elapsed = start.elapsed().as_secs_f64();
drop(db);
drop(tmp);
(next * per_op) as f64 / (1024.0 * 1024.0) / elapsed
}
struct Summary {
label: u64,
median: f64,
lo: f64,
hi: f64,
spread: Option<f64>,
stability: &'static str,
}
fn summarize(label: u64, mut samples: Vec<f64>) -> Summary {
let (lo, hi) = common::min_max(&samples);
let median = common::median(&mut samples);
let (spread, stability) = if median.is_finite() && median > 0.0 {
let s = (hi - lo) / median;
(
Some(s),
if s > UNSTABLE_SPREAD {
"unstable"
} else {
"stable"
},
)
} else {
(None, "unknown")
};
Summary {
label,
median,
lo,
hi,
spread,
stability,
}
}
fn num(x: f64) -> String {
if x.is_finite() {
format!("{x:.3}")
} else {
"null".to_string()
}
}
fn opt_num(x: Option<f64>) -> String {
x.map_or_else(|| "null".to_string(), num)
}
fn row(key: &str, s: &Summary, extra: &str) -> String {
format!(
"{{\"{key}\":{},{extra}\"median\":{},\"min\":{},\"max\":{},\"spread_ratio\":{},\"stability\":\"{}\"}}",
s.label,
num(s.median),
num(s.lo),
num(s.hi),
opt_num(s.spread),
s.stability,
)
}
fn storage_root() -> String {
let probe = common::TempDb::new("root-probe");
let dir = probe.path().parent().unwrap_or_else(|| probe.path());
dir.display().to_string()
}
fn json_str(s: &str) -> String {
let clean: String = s.chars().filter(|c| !c.is_control()).collect();
format!("\"{}\"", clean.replace('\\', "\\\\").replace('"', "\\\""))
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let smoke = args.iter().any(|a| a == "--test" || a == "--quick");
let reps = if smoke {
2
} else {
env_u64("REGOLITH_BENCH_REPS", 7)
} as usize;
let rep_ms = if smoke {
60
} else {
env_u64("REGOLITH_BENCH_REP_MS", 400)
};
let bulk_mib = if smoke {
2
} else {
env_u64("REGOLITH_BENCH_BULK_MIB", 32)
};
let dur = Duration::from_millis(rep_ms);
let root = storage_root();
println!("write batches, {reps} reps x {rep_ms} ms, value {VALUE_BYTES} B");
println!(" storage root: {root}");
let mut batch_summaries = Vec::with_capacity(BATCH_SIZES.len());
for batch_ops in BATCH_SIZES {
let samples: Vec<f64> = (0..reps).map(|_| batch_rep(batch_ops, dur)).collect();
let s = summarize(batch_ops as u64, samples);
println!(
" {:>3} op(s)/batch: median {:>10.0} ops/s min {:>10.0} max {:>10.0} spread {} {}",
s.label,
s.median,
s.lo,
s.hi,
s.spread
.map_or_else(|| "n/a".to_string(), |v| format!("{v:.2}")),
s.stability,
);
batch_summaries.push(s);
}
let bulk_reps = if smoke { 1 } else { reps.min(5) };
let samples: Vec<f64> = (0..bulk_reps).map(|_| bulk_rep(bulk_mib)).collect();
let bulk = summarize(bulk_mib, samples);
println!(
" bulk load {} MiB payload ({} B values, {} ops/batch, flush included): median {:.1} MiB/s min {:.1} max {:.1} {}",
bulk_mib, BULK_VALUE_BYTES, BULK_BATCH_OPS, bulk.median, bulk.lo, bulk.hi, bulk.stability,
);
if smoke {
println!(" smoke run (--test): metrics not emitted");
return;
}
let root_json = json_str(&root);
let rows: Vec<String> = batch_summaries
.iter()
.map(|s| row("batch_ops", s, "\"unit\":\"ops_per_sec\","))
.collect();
let json = format!(
"{{\"metric\":\"write_batch\",\"value_bytes\":{VALUE_BYTES},\"reps\":{reps},\"rep_ms\":{rep_ms},\"unstable_spread_threshold\":{UNSTABLE_SPREAD},\"storage_root\":{root_json},\"by_batch_size\":[{}],\"bulk_load\":{}}}",
rows.join(","),
row(
"payload_mib",
&bulk,
&format!(
"\"unit\":\"mib_per_sec\",\"reps\":{bulk_reps},\"value_bytes\":{BULK_VALUE_BYTES},\"batch_ops\":{BULK_BATCH_OPS},\"includes_close_flush\":true,"
),
),
);
common::write_family("batch", &json);
}