use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Instant;
use znippy_plugin_git::index_layout::{
IndexEntry, ObjType, ObjectIndex, OneTableFourColumns, Rng, synthetic_entries,
};
use znippy_plugin_git::read_stack::{ObjectReadStack, RebuildTriggers};
type Stack = ObjectReadStack<OneTableFourColumns>;
const LOAD_QUIET: f64 = 1.5;
fn loadavg() -> (f64, String) {
let s = fs::read_to_string("/proc/loadavg").unwrap_or_default();
let one = s
.split_whitespace()
.next()
.and_then(|x| x.parse::<f64>().ok())
.unwrap_or(f64::NAN);
(one, s.trim().to_string())
}
#[derive(Clone, Copy, Default, Debug)]
struct Io {
wchar: u64,
write_bytes: u64,
}
impl Io {
fn now() -> Io {
let mut io = Io::default();
let s = fs::read_to_string("/proc/self/io").unwrap_or_default();
for line in s.lines() {
let mut it = line.split(':');
let (Some(k), Some(v)) = (it.next(), it.next()) else {
continue;
};
let v = v.trim().parse::<u64>().unwrap_or(0);
match k {
"wchar" => io.wchar = v,
"write_bytes" => io.write_bytes = v,
_ => {}
}
}
io
}
fn since(self, t0: Io) -> Io {
Io {
wchar: self.wchar.saturating_sub(t0.wchar),
write_bytes: self.write_bytes.saturating_sub(t0.write_bytes),
}
}
}
fn cpu_ns() -> u64 {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut ts) };
ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
}
fn disk_bytes(path: &Path) -> u64 {
let Ok(md) = fs::metadata(path) else {
return 0;
};
if md.is_file() {
return md.len();
}
fs::read_dir(path)
.map(|rd| {
rd.filter_map(|e| e.ok())
.map(|e| disk_bytes(&e.path()))
.sum()
})
.unwrap_or(0)
}
#[derive(Clone, Copy)]
struct Stat {
med: f64,
lo: f64,
hi: f64,
}
impl Stat {
fn of(mut v: Vec<f64>) -> Stat {
assert!(!v.is_empty(), "a statistic over no samples is not a number");
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
Stat {
med: v[v.len() / 2],
lo: v[0],
hi: v[v.len() - 1],
}
}
fn band(&self) -> f64 {
if self.med == 0.0 {
0.0
} else {
(self.hi - self.lo) / self.med
}
}
}
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[v.len() / 2]
}
struct ArmRun {
wall_ms: Vec<f64>,
cpu_ms: Vec<f64>,
wchar: Vec<u64>,
write_bytes: Vec<u64>,
rebuilds: u64,
disk: u64,
rows: u64,
rebuild_debt_ms: f64,
}
impl ArmRun {
fn total_wall(&self) -> f64 {
self.wall_ms.iter().sum()
}
fn total_wchar(&self) -> u64 {
self.wchar.iter().sum()
}
fn slice_mean(v: &[f64], a: usize, b: usize) -> f64 {
let s: f64 = v[a..b].iter().sum();
s / (b - a) as f64
}
}
fn arm_with_tail(
dir: &Path,
base: &[IndexEntry],
pushes: &[Vec<IndexEntry>],
sample: &[Vec<u8>],
) -> ArmRun {
let _ = fs::remove_dir_all(dir);
fs::create_dir_all(dir).expect("arm A scratch");
let tail = dir.join("tail.redb");
let stack = Stack::open(
&tail,
RebuildTriggers::default(),
znippy_plugin_git::DEFAULT_REDB_CACHE_BYTES,
)
.expect("open tail");
for chunk in base.chunks(50_000) {
stack.append(chunk).expect("seed the tail");
}
stack.rebuild().expect("seal the seeded projection");
let rebuilds0 = stack.stats().rebuilds;
let mut run = ArmRun {
wall_ms: Vec::new(),
cpu_ms: Vec::new(),
wchar: Vec::new(),
write_bytes: Vec::new(),
rebuilds: 0,
disk: 0,
rows: 0,
rebuild_debt_ms: 0.0,
};
for p in pushes {
let io0 = Io::now();
let c0 = cpu_ns();
let t0 = Instant::now();
stack.append(p).expect("append a push");
let wall = t0.elapsed();
let cpu = cpu_ns() - c0;
let io = Io::now().since(io0);
run.wall_ms.push(wall.as_secs_f64() * 1e3);
run.cpu_ms.push(cpu as f64 / 1e6);
run.wchar.push(io.wchar);
run.write_bytes.push(io.write_bytes);
}
let pre = stack.stats();
let t0 = Instant::now();
stack.rebuild().expect("the deferred rebuild");
run.rebuild_debt_ms = t0.elapsed().as_secs_f64() * 1e3;
assert!(
pre.unabsorbed_rows > 0 || pre.rebuilds > rebuilds0,
"arm A owed no catch-up after {} pushes and never rebuilt — the appends \
did not reach the tail",
pushes.len()
);
let st = stack.stats();
run.rebuilds = st.rebuilds - rebuilds0 - 1;
run.rows = st.total_rows;
run.disk = disk_bytes(dir);
for oid in sample {
let got = stack.lookup(oid);
assert!(
got.is_some(),
"arm A lost {} — the tail is supposed to answer for everything ever appended",
hex::encode(oid)
);
}
run
}
fn arm_no_tail(
dir: &Path,
base: &[IndexEntry],
pushes: &[Vec<IndexEntry>],
sample: &[Vec<u8>],
) -> ArmRun {
let _ = fs::remove_dir_all(dir);
fs::create_dir_all(dir).expect("arm B scratch");
let index_path = dir.join("index.arrow");
let mut rows: Vec<IndexEntry> = base.to_vec();
let seed = OneTableFourColumns::build(&rows).expect("seed the index");
write_durable(&index_path, seed.ipc_slice());
drop(seed);
let mut run = ArmRun {
wall_ms: Vec::new(),
cpu_ms: Vec::new(),
wchar: Vec::new(),
write_bytes: Vec::new(),
rebuilds: 0,
disk: 0,
rows: 0,
rebuild_debt_ms: 0.0,
};
let mut last: Option<OneTableFourColumns> = None;
for p in pushes {
let io0 = Io::now();
let c0 = cpu_ns();
let t0 = Instant::now();
rows.extend_from_slice(p);
let idx = OneTableFourColumns::build(&rows).expect("rebuild the index");
write_durable(&index_path, idx.ipc_slice());
let wall = t0.elapsed();
let cpu = cpu_ns() - c0;
let io = Io::now().since(io0);
run.wall_ms.push(wall.as_secs_f64() * 1e3);
run.cpu_ms.push(cpu as f64 / 1e6);
run.wchar.push(io.wchar);
run.write_bytes.push(io.write_bytes);
run.rebuilds += 1;
last = Some(idx);
}
let idx = last.expect("at least one push");
run.rows = idx.len() as u64;
run.disk = disk_bytes(dir);
let on_disk = fs::metadata(&index_path).expect("arm B index exists").len();
assert_eq!(
on_disk,
idx.ipc_slice().len() as u64,
"arm B's file is {on_disk} bytes but its index is {} — the write did not land",
idx.ipc_slice().len()
);
assert!(
run.total_wchar() >= idx.ipc_slice().len() as u64,
"arm B charged {} write bytes for an index of {} — a rewrite cannot cost less than one copy",
run.total_wchar(),
idx.ipc_slice().len()
);
for oid in sample {
assert!(
idx.lookup(oid).is_some(),
"arm B lost {} — a full rebuild cannot drop a row it was given",
hex::encode(oid)
);
}
run
}
fn write_durable(path: &Path, bytes: &[u8]) {
let tmp = path.with_extension("tmp");
{
let mut f = fs::File::create(&tmp).expect("create the temp index");
f.write_all(bytes).expect("write the index");
f.sync_all().expect("fsync the index");
}
fs::rename(&tmp, path).expect("rename the index into place");
if let Some(d) = path.parent()
&& let Ok(dh) = fs::File::open(d)
{
let _ = dh.sync_all();
}
}
fn load_real(path: &Path, oid_len: usize, shuffle: bool, seed: u64) -> Vec<IndexEntry> {
let text = fs::read_to_string(path)
.unwrap_or_else(|e| panic!("reading the real-object dump {}: {e}", path.display()));
let mut out: Vec<IndexEntry> = Vec::new();
let mut bases: Vec<Vec<u8>> = Vec::new();
let mut off = 12u64;
for line in text.lines() {
let f: Vec<&str> = line.split_whitespace().collect();
if f.len() < 5 {
continue;
}
let Ok(oid) = hex::decode(f[0]) else { continue };
if oid.len() != oid_len {
continue;
}
let obj_type = match f[1] {
"commit" => ObjType::Commit,
"tree" => ObjType::Tree,
"blob" => ObjType::Blob,
"tag" => ObjType::Tag,
_ => continue,
};
let uncompressed_size: u64 = f[2].parse().unwrap_or(0);
let len: u64 = f[3].parse().unwrap_or(0);
let base = hex::decode(f[4]).unwrap_or_default();
out.push(IndexEntry {
oid,
offset: off,
len,
obj_type,
uncompressed_size,
delta_base: 0,
});
bases.push(base);
off += len.max(1);
}
let mut where_: std::collections::HashMap<&[u8], u64> =
std::collections::HashMap::with_capacity(out.len() * 2);
for e in &out {
where_.insert(e.oid.as_slice(), e.offset);
}
let resolved: Vec<u64> = bases
.iter()
.map(|b| {
if b.iter().all(|&x| x == 0) {
0
} else {
where_.get(b.as_slice()).copied().unwrap_or(0)
}
})
.collect();
for (e, base_off) in out.iter_mut().zip(resolved) {
e.delta_base = base_off;
}
if shuffle {
let mut rng = Rng(seed);
for i in (1..out.len()).rev() {
let j = (rng.next_u64() % (i as u64 + 1)) as usize;
out.swap(i, j);
}
}
out
}
fn arg(args: &[String], key: &str) -> Option<String> {
args.iter()
.find_map(|a| a.strip_prefix(&format!("{key}=")).map(|v| v.to_string()))
}
fn main() {
let thp = znippy_zoomies::stree::thp_enable_for_process();
eprintln!("thp_enabled={}", thp);
let args: Vec<String> = std::env::args().skip(1).collect();
let bases: Vec<usize> = arg(&args, "bases")
.unwrap_or_else(|| "10000,100000,1000000".into())
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
let pushes_n: usize = arg(&args, "pushes").and_then(|s| s.parse().ok()).unwrap_or(50);
let push_n: usize = arg(&args, "push").and_then(|s| s.parse().ok()).unwrap_or(300);
let runs: usize = arg(&args, "runs").and_then(|s| s.parse().ok()).unwrap_or(3);
let oid_len: usize = arg(&args, "oid").and_then(|s| s.parse().ok()).unwrap_or(20);
let seed: u64 = arg(&args, "seed").and_then(|s| s.parse().ok()).unwrap_or(0xB0BA);
let scratch = PathBuf::from(
arg(&args, "dir").unwrap_or_else(|| "/home/rickard/scratch/gitbench/tailwrite".into()),
);
let real = arg(&args, "real").map(PathBuf::from);
let shuffle = arg(&args, "shuffle").map(|v| v != "0").unwrap_or(true);
let (l0, lfull) = loadavg();
println!("# tail_write_bench — what the redb tail buys on the write side");
println!(
"# loadavg at start: {lfull} ({})",
if l0 <= LOAD_QUIET { "quiet" } else { "LOADED" }
);
println!(
"# workload: {} pushes of {push_n} objects, {runs} runs/cell, oid {oid_len}B, arm order rotates per run",
pushes_n
);
let corpus: Option<Vec<IndexEntry>> = real.as_ref().map(|p| {
let v = load_real(p, oid_len, shuffle, seed);
println!(
"# real corpus: {} objects from {}, mean stored len {} B, arrival order {}",
v.len(),
p.display(),
v.iter().map(|e| e.len).sum::<u64>() / v.len().max(1) as u64,
if shuffle {
"SHUFFLED (what a push looks like)"
} else {
"oid-sorted (flatters the tail's B-tree — see load_real)"
}
);
v
});
let kind = if corpus.is_some() { "real" } else { "synthetic" };
println!("# entries: {kind}");
println!();
for &base_n in &bases {
let total = base_n + pushes_n * push_n;
let all: Vec<IndexEntry> = match &corpus {
Some(c) => {
if c.len() < total {
println!(
"## base {base_n}: SKIPPED — the real corpus has {} objects, this cell needs {total}",
c.len()
);
continue;
}
c[..total].to_vec()
}
None => synthetic_entries(total, oid_len, seed),
};
let base: Vec<IndexEntry> = all[..base_n].to_vec();
let pushes: Vec<Vec<IndexEntry>> = all[base_n..]
.chunks(push_n)
.map(|c| c.to_vec())
.collect();
let sample: Vec<Vec<u8>> = [
base.first(),
base.last(),
pushes[0].first(),
pushes[0].last(),
pushes[pushes.len() - 1].first(),
pushes[pushes.len() - 1].last(),
]
.iter()
.filter_map(|e| e.map(|e| e.oid.clone()))
.collect();
let push_bytes: u64 = pushes[0].iter().map(|e| e.len).sum();
println!(
"## base {base_n} objects ({kind}) — {} pushes × {push_n}, {} stored bytes per push",
pushes.len(),
push_bytes
);
let mut a_runs: Vec<ArmRun> = Vec::new();
let mut b_runs: Vec<ArmRun> = Vec::new();
let mut loads: Vec<String> = Vec::new();
for run in 0..runs {
let a_first = run % 2 == 0;
let dir_a = scratch.join(format!("a-{base_n}-{run}"));
let dir_b = scratch.join(format!("b-{base_n}-{run}"));
let (la, sa) = loadavg();
loads.push(format!("run{run}:{sa}"));
if la > LOAD_QUIET {
println!(" [LOADED] run {run} starts at loadavg {la}");
}
if a_first {
a_runs.push(arm_with_tail(&dir_a, &base, &pushes, &sample));
b_runs.push(arm_no_tail(&dir_b, &base, &pushes, &sample));
} else {
b_runs.push(arm_no_tail(&dir_b, &base, &pushes, &sample));
a_runs.push(arm_with_tail(&dir_a, &base, &pushes, &sample));
}
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
}
report("A with tail ", &a_runs, pushes.len(), base_n as u64);
report("B no tail ", &b_runs, pushes.len(), base_n as u64);
curve(&a_runs[0], &b_runs[0], base_n, push_n);
let debt = median(a_runs.iter().map(|r| r.rebuild_debt_ms).collect());
let period = (RebuildTriggers::DEFAULT_TAIL_BYTES as f64 / push_bytes.max(1) as f64).max(1.0);
let a_per_push = median(a_runs.iter().map(|r| r.total_wall()).collect()) / pushes.len() as f64;
println!(
" A's deferred rebuild: {debt:8.1} ms over {} rows, one per {period:.1} pushes at this \
push size → +{:.3} ms/push amortised (per-push {:.3} → {:.3} ms)",
a_runs[0].rows,
debt / period,
a_per_push,
a_per_push + debt / period,
);
let a_tot = median(a_runs.iter().map(|r| r.total_wall()).collect());
let b_tot = median(b_runs.iter().map(|r| r.total_wall()).collect());
let a_w = median(a_runs.iter().map(|r| r.total_wchar() as f64).collect());
let b_w = median(b_runs.iter().map(|r| r.total_wchar() as f64).collect());
println!(
" ratio B/A: wall {:.1}×, bytes written {:.1}×",
b_tot / a_tot,
b_w / a_w
);
for l in &loads {
println!(" loadavg {l}");
}
println!();
}
}
fn curve(a: &ArmRun, b: &ArmRun, base_n: usize, push_n: usize) {
let n = a.wall_ms.len();
let step = (n / 8).max(1);
println!(
" {:>6} {:>10} {:>10} {:>12} {:>10} {:>12}",
"push", "repo rows", "A wall ms", "A bytes", "B wall ms", "B bytes"
);
let mut i = 0;
while i < n {
println!(
" {:>6} {:>10} {:>10.3} {:>12} {:>10.3} {:>12}",
i,
base_n + (i + 1) * push_n,
a.wall_ms[i],
a.wchar[i],
b.wall_ms[i],
b.wchar[i]
);
i += step;
}
if n > 0 && (n - 1) % step != 0 {
let i = n - 1;
println!(
" {:>6} {:>10} {:>10.3} {:>12} {:>10.3} {:>12}",
i,
base_n + (i + 1) * push_n,
a.wall_ms[i],
a.wchar[i],
b.wall_ms[i],
b.wchar[i]
);
}
}
fn report(name: &str, runs: &[ArmRun], n_pushes: usize, base_n: u64) {
for r in runs {
assert_eq!(
r.wall_ms.len(),
n_pushes,
"{name} timed {} pushes, not {n_pushes} — a missing sample would flatten the curve",
r.wall_ms.len()
);
assert!(
r.rows > base_n,
"{name} ended with {} rows over a base of {base_n} — the pushes did not land",
r.rows
);
}
let wall = Stat::of(runs.iter().map(|r| r.total_wall()).collect());
let cpu = Stat::of(runs.iter().map(|r| r.cpu_ms.iter().sum::<f64>()).collect());
let wchar = Stat::of(runs.iter().map(|r| r.total_wchar() as f64).collect());
let wbytes = Stat::of(
runs.iter()
.map(|r| r.write_bytes.iter().sum::<u64>() as f64)
.collect(),
);
let third = (n_pushes / 3).max(1);
let first3 = Stat::of(
runs.iter()
.map(|r| ArmRun::slice_mean(&r.wall_ms, 0, third))
.collect(),
);
let last3 = Stat::of(
runs.iter()
.map(|r| ArmRun::slice_mean(&r.wall_ms, n_pushes - third, n_pushes))
.collect(),
);
let r0 = &runs[0];
println!(
" {name} total wall {:8.1} ms [{:.1}–{:.1}, band {:>5.1}%] cpu {:8.1} ms \
wchar {:>12} B [band {:>5.1}%] write_bytes {:>12} B disk {:>12} B rows {} rebuilds {}",
wall.med,
wall.lo,
wall.hi,
wall.band() * 100.0,
cpu.med,
wchar.med as u64,
wchar.band() * 100.0,
wbytes.med as u64,
r0.disk,
r0.rows,
r0.rebuilds,
);
println!(
" {:<15} per push: mean {:8.3} ms first {third} pushes {:8.3} ms last {third} pushes {:8.3} ms \
growth last/first {:5.2}× bytes/push {:>10} B",
"",
wall.med / n_pushes as f64,
first3.med,
last3.med,
last3.med / first3.med,
(wchar.med / n_pushes as f64) as u64,
);
}