#[allow(dead_code)]
#[path = "../../tests/common/mod.rs"]
mod common;
mod disk;
mod knot;
mod measure;
mod phases;
mod report;
mod seed;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::json;
use crate::knot::{Hosted, Knot};
use crate::measure::{Ran, Usage};
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Point {
pub commits: usize,
pub refs: usize,
pub tickets: usize,
pub versions: usize,
pub repos: usize,
}
const BASE: Point = Point {
commits: 100,
refs: 4,
tickets: 0,
versions: 1,
repos: 1,
};
const CHURN: usize = 5;
fn axes(big: bool) -> Vec<(&'static str, Vec<(usize, Point)>)> {
let pick = |small: &[usize], large: &[usize]| if big { large.to_vec() } else { small.to_vec() };
let vary = |values: Vec<usize>, set: fn(&mut Point, usize)| {
values
.into_iter()
.map(|value| {
let mut point = BASE;
set(&mut point, value);
(value, point)
})
.collect::<Vec<_>>()
};
vec![
("base", vec![(0, BASE)]),
(
"commits",
vary(pick(&[1_000, 10_000], &[1_000, 10_000, 100_000]), |p, v| {
p.commits = v
}),
),
(
"refs",
vary(pick(&[100, 1_000], &[100, 1_000, 10_000]), |p, v| {
p.refs = v
}),
),
(
"tickets",
vary(pick(&[100, 500], &[10, 100, 1_000, 5_000]), |p, v| {
p.tickets = v
}),
),
(
"versions",
vary(pick(&[10, 50], &[1, 10, 100]), |p, v| {
p.tickets = 10;
p.versions = v;
}),
),
(
"repos",
vary(pick(&[10, 30], &[10, 50, 200]), |p, v| p.repos = v),
),
]
}
struct Settings {
binary: PathBuf,
nginx: Option<PathBuf>,
work: PathBuf,
linux: Option<PathBuf>,
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Record {
pub axis: String,
pub value: usize,
pub point: Option<Point>,
pub op: String,
pub ok: bool,
pub usage: Usage,
pub client: Option<Usage>,
pub phases: phases::Phases,
pub written: disk::Written,
pub generation: Option<Generation>,
pub activation: serde_json::Value,
pub reload_s: Option<f64>,
pub disk_bytes: u64,
pub generations_on_disk: usize,
pub error: Option<String>,
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Generation {
pub id: String,
pub files: u64,
pub disk_bytes: u64,
pub nginx_conf_bytes: u64,
pub smart: BTreeMap<String, u64>,
pub sharing: Option<disk::Sharing>,
}
fn main() {
if let Some(file) = std::env::var_os(measure::USAGE_ENV) {
measure::wrapper(Path::new(&file));
}
if let Some(file) = std::env::var_os(measure::SSH_USAGE_ENV) {
measure::ssh_stand_in(Path::new(&file));
}
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let var = |name: &str| std::env::var_os(name).map(PathBuf::from);
let settings = Settings {
binary: var("KNOT_STATIC_BIN")
.unwrap_or_else(|| PathBuf::from(env!("CARGO_BIN_EXE_knot-static"))),
nginx: var("NGINX_BINARY"),
work: var("KS_BENCH_WORK").unwrap_or_else(|| manifest.join("target/ks-bench")),
linux: var("KS_BENCH_LINUX"),
};
let preset = std::env::var("KS_BENCH_PRESET").unwrap_or_else(|_| "small".into());
let big = preset == "big";
let label = std::env::var("KS_BENCH_LABEL")
.unwrap_or_else(|_| format!("{}-{preset}", std::env::consts::OS));
let results = var("KS_BENCH_RESULTS").unwrap_or_else(|| manifest.join("bench/results"));
let only: Option<Vec<String>> = std::env::var("KS_BENCH_AXES")
.ok()
.map(|axes| axes.split(',').map(str::to_string).collect());
let wanted = |axis: &str| {
only.as_ref()
.is_none_or(|only| only.iter().any(|a| a == axis))
};
std::fs::create_dir_all(&results).unwrap();
let jsonl = results.join(format!("{label}.jsonl"));
let mut records = Vec::new();
let mut sink = std::fs::File::create(&jsonl).unwrap();
let mut keep = |record: Record| {
use std::io::Write as _;
eprintln!(
"{:>9} {:>7} {:<22} {:>8.3}s {:>7.1} MiB rss {:>7.1} MiB anon {:>9.2} MiB written",
record.axis,
record.value,
record.op,
record.usage.wall_s,
record.usage.max_rss_bytes as f64 / 1048576.0,
record.usage.peak_anon_bytes.unwrap_or(0) as f64 / 1048576.0,
record.written.bytes as f64 / 1048576.0,
);
writeln!(sink, "{}", serde_json::to_string(&record).unwrap()).unwrap();
records.push(record);
};
for (axis, points) in axes(big) {
if !wanted(axis) {
continue;
}
for (value, point) in points {
let scratch = settings.work.join(format!("{axis}-{value}"));
let _ = std::fs::remove_dir_all(&scratch);
let churn = if axis == "base" { CHURN } else { 0 };
abort_on_panic(axis, value, &mut keep, |keep| {
run_point(&settings, &scratch, axis, value, point, None, churn, keep)
});
let _ = std::fs::remove_dir_all(&scratch);
}
}
if wanted("linux")
&& let Some(linux) = &settings.linux
{
let scratch = settings.work.join("linux");
let _ = std::fs::remove_dir_all(&scratch);
abort_on_panic("linux", 0, &mut keep, |keep| {
run_point(&settings, &scratch, "linux", 0, BASE, Some(linux), 0, keep)
});
let _ = std::fs::remove_dir_all(&scratch);
}
drop(keep);
let table = results.join(format!("{label}.md"));
std::fs::write(&table, report::markdown(&label, &records)).unwrap();
eprintln!("wrote {} and {}", jsonl.display(), table.display());
}
fn abort_on_panic<K: FnMut(Record)>(
axis: &str,
value: usize,
keep: &mut K,
point: impl FnOnce(&mut K),
) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| point(&mut *keep)));
if let Err(panic) = outcome {
let error = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| panic.downcast_ref::<&str>().map(|text| text.to_string()))
.unwrap_or_default();
keep(Record {
axis: axis.to_string(),
value,
op: "aborted".into(),
error: Some(error),
..Record::default()
});
}
}
#[allow(clippy::too_many_arguments)]
fn run_point(
settings: &Settings,
scratch: &Path,
axis: &str,
value: usize,
point: Point,
upstream: Option<&Path>,
churn: usize,
keep: &mut impl FnMut(Record),
) {
let knot = Knot::new(
&scratch.join("knot"),
&settings.binary,
settings.nginx.as_deref(),
);
let source = scratch.join("source.git");
let seeded = std::time::Instant::now();
let commits = match upstream {
Some(upstream) => {
let cloned = common::git_command(scratch)
.args(["clone", "-q", "--bare"])
.arg(upstream)
.arg(&source)
.output()
.unwrap();
assert!(
cloned.status.success(),
"clone {}: {cloned:?}",
upstream.display()
);
common::git(&source, ["branch", "-m", "master", "main"]);
common::git(&source, ["rev-list", "--count", "main"])
.parse()
.unwrap()
}
None => {
seed::source(&source, point.commits, point.refs);
point.commits
}
};
let mut step = Step {
axis,
value,
point: upstream.is_none().then_some(point),
knot: &knot,
repo: String::new(),
};
let mut repos: Vec<Hosted> = Vec::new();
if upstream.is_some() {
// import runs a git process per small blob, which takes hours on linux. the packs get linked in instead.
let hosted = knot.create("r0");
let packs = hosted.path.join("objects/pack");
for pack in std::fs::read_dir(source.join("objects/pack")).unwrap() {
let pack = pack.unwrap().path();
std::fs::hard_link(&pack, packs.join(pack.file_name().unwrap())).unwrap();
}
let refs = common::git(
&source,
["for-each-ref", "--format=create %(refname) %(objectname)"],
);
let mut update = common::git_command(&hosted.path);
update.args(["update-ref", "--stdin"]);
measure::run(&update, Some(format!("{refs}\n").as_bytes())).ok("update-ref");
step.repo = hosted.did.clone();
repos.push(hosted);
}
for index in repos.len()..point.repos {
let rkey = format!("r{index}");
let before = disk::scan(&knot.root, false);
let (ran, hosted) = knot.import(&rkey, &source);
if index == 0 {
step.repo = hosted.did.clone();
keep(step.record("import", &ran, None, &before, None));
}
repos.push(hosted);
}
let first = &repos[0];
seed::tickets(
&first.path,
&first.did,
knot::OWNER,
point.tickets,
point.versions,
);
eprintln!("{axis} {value}: seeded in {:.1?}", seeded.elapsed());
// the hosted kernel has had maintenance. the cold build marks through its bitmap.
if upstream.is_some() {
let before = disk::scan(&knot.root, false);
let ran = knot.maintenance();
keep(step.record("maintenance-first", &ran, None, &before, None));
}
let before = disk::scan(&knot.root, false);
let (ran, generation) = knot.generate(None);
keep(step.record("generate-cold", &ran, None, &before, generation.as_deref()));
let generation = generation.expect("a cold generate builds a generation");
let before = disk::scan(&knot.root, false);
let ran = knot.activate(&generation);
keep(step.record("activate-cold", &ran, None, &before, None));
if knot.serve() {
let before = disk::scan(&knot.root, false);
let ran = knot.nginx_test().unwrap();
keep(step.record("nginx-t", &ran, None, &before, None));
}
seed::advance(&first.path, commits);
let before = disk::scan(&knot.root, false);
let (ran, generation) = knot.generate(Some(&first.did));
keep(step.record(
"generate-incremental",
&ran,
None,
&before,
generation.as_deref(),
));
let generation = generation.expect("a moved ref builds a generation");
let before = disk::scan(&knot.root, false);
let ran = knot.activate(&generation);
let mut record = step.record("activate-incremental", &ran, None, &before, None);
record.reload_s = knot.reload(&first.did);
keep(record);
common::git(
&source,
["fetch", "-q", first.path.to_str().unwrap(), "+main:main"],
);
seed::advance(&source, commits + 1);
let before = disk::scan(&knot.root, false);
let previous = knot.active_generation();
let (client, usage) = knot.push(&source, "r0");
let mut record = step.record_usage("push", &client, usage, &before, previous);
record.client = Some(client.usage);
record.reload_s = knot.reload(&first.did);
keep(record);
if upstream.is_some() {
for (op, (commits_added, at)) in [("push-hit", (1, 2)), ("push-miss", (6, 3))] {
for index in 0..commits_added {
seed::advance(&source, commits + at * 10 + index);
}
let before = disk::scan(&knot.root, false);
let previous = knot.active_generation();
let (client, usage) = knot.push(&source, "r0");
let mut record = step.record_usage(op, &client, usage, &before, previous);
record.client = Some(client.usage);
record.reload_s = knot.reload(&first.did);
keep(record);
}
let before = disk::scan(&knot.root, false);
let ran = knot.maintenance();
keep(step.record("maintenance-next", &ran, None, &before, None));
return;
}
for round in 0..churn.max(1) {
let before = disk::scan(&knot.root, false);
let ticket = json!({
"repo": first.did,
"collection": "org.tangled.track.ticket",
"record": {
"$type": "org.tangled.track.ticket",
"title": format!("bench {round}"),
"body": "measured",
"createdAt": "2026-01-01T00:00:00Z",
},
});
let previous = knot.active_generation();
let ran = knot.write("createRecord", &first.did, &ticket);
let mut record = step.record("cob-create", &ran, previous, &before, None);
if round > 0 {
record.axis = "churn".into();
record.value = round;
}
record.reload_s = knot.reload(&first.did);
keep(record);
}
// ticket 1 comes from the seed, or else from the first write above.
let collection = "org.tangled.track.ticket";
let edit = json!({
"repo": first.did,
"collection": collection,
"rkey": "1",
"swapRecord": knot.current_cid(&first.did, collection, "1").expect("ticket 1 is served"),
"record": {
"$type": "org.tangled.track.ticket",
"title": "edited",
"body": "a new version",
"createdAt": "2026-01-01T00:00:00Z",
},
});
let before = disk::scan(&knot.root, false);
let previous = knot.active_generation();
let ran = knot.write("putRecord", &first.did, &edit);
let mut record = step.record("cob-put", &ran, previous, &before, None);
record.reload_s = knot.reload(&first.did);
keep(record);
}
struct Step<'a> {
axis: &'a str,
value: usize,
point: Option<Point>,
knot: &'a Knot,
repo: String,
}
impl Step<'_> {
fn record(
&self,
op: &str,
ran: &Ran,
previous: Option<String>,
before: &disk::Tree,
generation: Option<&str>,
) -> Record {
let mut record = self.record_usage(op, ran, ran.usage, before, previous);
if let Some(generation) = generation {
record.generation = Some(self.generation(generation, self.knot.active_generation()));
}
record
}
fn record_usage(
&self,
op: &str,
ran: &Ran,
usage: Usage,
before: &disk::Tree,
previous: Option<String>,
) -> Record {
let after = disk::scan(&self.knot.root, false);
let active = self.knot.active_generation();
let generation = previous
.is_some()
.then(|| active.clone())
.flatten()
.filter(|active| previous.as_ref() != Some(active))
.map(|active| self.generation(&active, previous.clone()));
Record {
axis: self.axis.to_string(),
value: self.value,
point: self.point,
op: op.to_string(),
ok: ran.succeeded(),
usage,
client: None,
phases: phases::parse(&ran.stderr()),
written: disk::written(before, &after),
generation,
activation: self.knot.activation_record()["telemetry"].clone(),
reload_s: None,
disk_bytes: after.disk_bytes(),
generations_on_disk: std::fs::read_dir(self.knot.output.join("generations"))
.map_or(0, |entries| entries.count()),
error: None,
}
}
fn generation(&self, id: &str, previous: Option<String>) -> Generation {
let dir = self.knot.generation_dir(id);
let tree = disk::scan(&dir, true);
let sharing = previous.filter(|previous| previous != id).map(|previous| {
let previous_dir = self.knot.generation_dir(&previous);
disk::sharing(&previous_dir, &disk::scan(&previous_dir, true), &dir, &tree)
});
let mount = dir.join("www/_git").join(common::repo_token(&self.repo));
let smart = std::fs::read_dir(mount.join("smart"))
.into_iter()
.flatten()
.flatten()
.filter_map(|entry| {
Some((
entry.file_name().to_string_lossy().into_owned(),
entry.metadata().ok()?.len(),
))
})
.collect();
Generation {
id: id.to_string(),
files: tree.files(),
disk_bytes: tree.disk_bytes(),
nginx_conf_bytes: disk::scan(&dir.join("nginx"), false).disk_bytes(),
smart,
sharing,
}
}
}