use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use git_storage_trait::GitOps as _;
use znippy_plugin_git::git_ops::GitStore;
fn anon_rss_kb() -> u64 {
let s = std::fs::read_to_string("/proc/self/smaps_rollup")
.expect("/proc/self/smaps_rollup (Linux 4.14+)");
for line in s.lines() {
if let Some(rest) = line.strip_prefix("Anonymous:") {
let kb = rest.trim().trim_end_matches("kB").trim();
return kb.parse().expect("Anonymous: is a kB count");
}
}
panic!("no `Anonymous:` line in smaps_rollup");
}
fn total_rss_kb() -> u64 {
let s = std::fs::read_to_string("/proc/self/smaps_rollup").unwrap_or_default();
for line in s.lines() {
if let Some(rest) = line.strip_prefix("Rss:") {
let kb = rest.trim().trim_end_matches("kB").trim();
return kb.parse().unwrap_or(0);
}
}
0
}
fn mallinfo_kb() -> Option<(u64, u64)> {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
{
let mi = unsafe { libc::mallinfo2() };
return Some((
(mi.uordblks as u64 + mi.hblkhd as u64) / 1024,
(mi.arena as u64 + mi.hblkhd as u64) / 1024,
));
}
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
None
}
fn malloc_trim() {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe {
libc::malloc_trim(0)
};
}
fn mb(kb: u64) -> f64 {
kb as f64 / 1024.0
}
fn loadavg() -> String {
std::fs::read_to_string("/proc/loadavg")
.unwrap_or_default()
.split_whitespace()
.take(3)
.collect::<Vec<_>>()
.join(" ")
}
fn copy_tree(from: &Path, to: &Path) -> Result<()> {
std::fs::create_dir_all(to)?;
for e in std::fs::read_dir(from)? {
let e = e?;
let src = e.path();
let dst = to.join(e.file_name());
if e.file_type()?.is_dir() {
copy_tree(&src, &dst)?;
} else {
std::fs::copy(&src, &dst)
.with_context(|| format!("copying {} to {}", src.display(), dst.display()))?;
}
}
Ok(())
}
fn synthetic_pack(n: usize) -> Vec<u8> {
use std::io::Write as _;
use sha1::Digest as _;
let mut out = Vec::with_capacity(n * 80 + 32);
out.extend_from_slice(b"PACK");
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&(n as u32).to_be_bytes());
for i in 0..n {
let mut body = [0u8; 64];
let mut s = (i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
for c in body.chunks_mut(8) {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
c.copy_from_slice(&s.to_le_bytes());
}
let mut size = body.len() as u64;
let mut b0 = ((3u8) << 4) | (size & 0x0F) as u8;
size >>= 4;
if size > 0 {
b0 |= 0x80;
}
out.push(b0);
while size > 0 {
let mut b = (size & 0x7F) as u8;
size >>= 7;
if size > 0 {
b |= 0x80;
}
out.push(b);
}
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
z.write_all(&body).expect("deflate a blob");
out.extend_from_slice(&z.finish().expect("finish a blob"));
}
let mut h = sha1::Sha1::new();
h.update(&out);
out.extend_from_slice(&h.finalize());
out
}
fn stride(all: &[Vec<u8>], want: usize) -> Vec<Vec<u8>> {
if all.is_empty() || want == 0 {
return Vec::new();
}
let step = (all.len() / want).max(1);
all.iter().step_by(step).take(want).cloned().collect()
}
fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let spec = args
.next()
.context("usage: <pack | synthetic:OBJECTS> <scratch dir> <N> [reads]")?;
let root = PathBuf::from(args.next().context("usage: <pack> <scratch dir> <N> [reads]")?);
let n: usize = args.next().unwrap_or_else(|| "25".into()).parse().unwrap_or(25);
let reads: usize = args.next().unwrap_or_else(|| "64".into()).parse().unwrap_or(64);
if root.exists() {
std::fs::remove_dir_all(&root).with_context(|| format!("clearing {}", root.display()))?;
}
std::fs::create_dir_all(&root)?;
let (map, built, label): (Option<memmap2::Mmap>, Vec<u8>, String) =
match spec.strip_prefix("synthetic:") {
Some(count) => {
let objects: usize = count.parse().context("synthetic:<objects>")?;
(None, synthetic_pack(objects), format!("synthetic, {objects} blobs"))
}
None => {
let pack_path = PathBuf::from(&spec);
let f = std::fs::File::open(&pack_path)
.with_context(|| format!("opening {}", pack_path.display()))?;
let m = unsafe { memmap2::Mmap::map(&f)? };
let label = pack_path.display().to_string();
(Some(m), Vec::new(), label)
}
};
let pack: &[u8] = match &map {
Some(m) => &m[..],
None => &built[..],
};
println!("=== RSS PER OPEN STORE ===");
println!("pack {:>14} B {label}", pack.len());
println!("scratch {}", root.display());
println!("stores {n} reads per store {reads}");
println!(
"explode {} redb cache {}",
std::env::var("ZNIPPY_GIT_EXPLODE").unwrap_or_else(|_| "(default)".into()),
std::env::var("ZNIPPY_GIT_REDB_CACHE_BYTES").unwrap_or_else(|_| "(default 64 MiB)".into()),
);
println!("loadavg {}", loadavg());
let seed = root.join("store-0");
let (objects, picks) = {
let store = GitStore::open(&seed, "rickard").context("opening the seed store")?;
let tx = store.put_pack(pack).context("the push")?;
store.wait_indexed();
let objects = store.object_count();
let extent = tx.extent.context("the push reported no extent")?;
let oids = store.oids_in_extent(extent).context("listing the pack's oids")?;
let picks = stride(&oids, reads);
(objects, picks)
};
drop(map);
drop(built);
println!("seeded {objects} objects, {} oids picked for reads", picks.len());
for i in 1..n {
copy_tree(&seed, &root.join(format!("store-{i}")))?;
}
malloc_trim();
let base = anon_rss_kb();
let base_total = total_rss_kb();
let (base_use, _) = mallinfo_kb().unwrap_or((0, 0));
println!(
"\nbaseline anon {:8.1} MB rss {:8.1} MB malloc in-use {:8.1} MB\n",
mb(base),
mb(base_total),
mb(base_use)
);
println!(
"{:>5} {:>12} {:>12} {:>12} {:>12} {:>12}",
"open", "anon MB", "Δ anon MB", "per store MB", "malloc MB", "rss MB"
);
let mut held = Vec::with_capacity(n);
let mut rows: Vec<(usize, u64)> = Vec::with_capacity(n);
let mut live: Vec<(usize, u64)> = Vec::with_capacity(n);
for i in 0..n {
let dir = root.join(format!("store-{i}"));
let store = GitStore::open(&dir, "rickard")
.with_context(|| format!("opening store {i}"))?;
for oid in &picks {
store
.content(oid)?
.with_context(|| format!("store {i}: an object of its own pack is absent"))?;
}
store.wait_indexed();
held.push(store);
malloc_trim();
let now = anon_rss_kb();
let (in_use, _) = mallinfo_kb().unwrap_or((0, 0));
rows.push((i + 1, now));
live.push((i + 1, in_use));
println!(
"{:>5} {:>12.1} {:>12.1} {:>12.1} {:>12.1} {:>12.1}",
i + 1,
mb(now),
mb(now.saturating_sub(base)),
mb(now.saturating_sub(base)) / (i + 1) as f64,
mb(in_use),
mb(total_rss_kb()),
);
}
let first = rows.first().copied().unwrap_or((1, base));
let last = rows.last().copied().unwrap_or((1, base));
let marginal = if last.0 > first.0 {
(last.1.saturating_sub(first.1)) as f64 / (last.0 - first.0) as f64
} else {
(last.1.saturating_sub(base)) as f64
};
let live_first = live.first().copied().unwrap_or((1, 0));
let live_last = live.last().copied().unwrap_or((1, 0));
let live_marginal = if live_last.0 > live_first.0 {
(live_last.1.saturating_sub(live_first.1)) as f64 / (live_last.0 - live_first.0) as f64
} else {
live_last.1 as f64
};
let mut steps: Vec<f64> = Vec::new();
for w in rows.windows(2) {
steps.push(w[1].1.saturating_sub(w[0].1) as f64);
}
let (lo, hi) = steps.iter().fold((f64::MAX, 0.0f64), |(l, h), s| (l.min(*s), h.max(*s)));
println!(
"\nSLOPE {:.1} MB per additional open store (stores {}→{}, \
{:.1} MB total over baseline)",
marginal / 1024.0,
first.0,
last.0,
mb(last.1.saturating_sub(base))
);
println!(
" {:.1} MB by malloc in-use (reachable bytes; agreement with the line \
above means a structure, not the arena)",
live_marginal / 1024.0
);
if !steps.is_empty() {
println!(
" per-open increments {:.1}–{:.1} MB, spread {:.1} % of the mean",
lo / 1024.0,
hi / 1024.0,
100.0 * (hi - lo) / (steps.iter().sum::<f64>() / steps.len() as f64).max(1.0)
);
}
println!(
" {:.1} bytes per object per open store ({objects} objects)",
marginal * 1024.0 / objects.max(1) as f64
);
println!(
" gunnar prices this at BYTES_PER_OPEN_REPO = 8.0 MB → measured/budgeted \
= {:.1}x",
marginal / 1024.0 / 8.0
);
drop(held);
let dropped = anon_rss_kb();
malloc_trim();
let trimmed = anon_rss_kb();
println!(
"\nafter drop {:8.1} MB anon ({:.1}% of the growth still resident)\nafter trim \
{:8.1} MB anon ({:.1}% still resident)",
mb(dropped),
100.0 * dropped.saturating_sub(base) as f64 / last.1.saturating_sub(base).max(1) as f64,
mb(trimmed),
100.0 * trimmed.saturating_sub(base) as f64 / last.1.saturating_sub(base).max(1) as f64,
);
println!("loadavg {}", loadavg());
Ok(())
}