use bashkit::Bash;
use std::time::{Duration, Instant};
fn calls() -> usize {
std::env::var("CPYTHON_LOAD_CALLS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4)
}
async fn tenant(id: usize) -> Result<Vec<Duration>, String> {
let mut bash = Bash::builder().cpython().build();
let mut latencies = Vec::with_capacity(calls());
for call in 0..calls() {
let script = format!(
"python3 -c 'import json, os\n\
p = \"/state.json\"\n\
s = json.load(open(p)) if os.path.exists(p) else {{\"id\": {id}, \"n\": 0}}\n\
assert s[\"id\"] == {id}, s\n\
s[\"n\"] += 1\n\
json.dump(s, open(p, \"w\"))\n\
print(s[\"id\"], s[\"n\"])'"
);
let t = Instant::now();
let r = bash.exec(&script).await.map_err(|e| e.to_string())?;
latencies.push(t.elapsed());
let want = format!("{id} {}\n", call + 1);
if r.exit_code != 0 || r.stdout != want.as_str() {
return Err(format!(
"tenant {id} call {call}: exit {} stdout {:?} stderr {:?}",
r.exit_code,
r.stdout.to_string(),
r.stderr.to_string()
));
}
}
Ok(latencies)
}
fn peak_rss_mb() -> Option<f64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let line = status.lines().find(|l| l.starts_with("VmHWM:"))?;
let kb: f64 = line.split_whitespace().nth(1)?.parse().ok()?;
Some(kb / 1024.0)
}
fn pct(sorted: &[Duration], p: f64) -> f64 {
let idx = ((sorted.len() as f64 - 1.0) * p).round() as usize;
sorted[idx].as_secs_f64() * 1e3
}
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let levels: Vec<usize> = std::env::args()
.skip(1)
.map(|a| a.parse().expect("concurrency level"))
.collect();
let levels = if levels.is_empty() {
vec![1, 16, 64, 256]
} else {
levels
};
bashkit::warm_up().expect("load CPython");
println!(
"| Tenants | Calls | Wall (s) | Calls/s | p50 (ms) | p99 (ms) | max (ms) | Failures | Peak RSS (MB) |"
);
println!(
"|---------|-------|----------|---------|----------|----------|----------|----------|---------------|"
);
let mut failed = false;
for n in levels {
let start = Instant::now();
let handles: Vec<_> = (0..n).map(|id| tokio::spawn(tenant(id))).collect();
let mut all = Vec::with_capacity(n * calls());
let mut failures = 0;
for h in handles {
match h.await.expect("tenant task") {
Ok(l) => all.extend(l),
Err(e) => {
failures += 1;
eprintln!("{e}");
}
}
}
let wall = start.elapsed().as_secs_f64();
all.sort();
failed |= failures > 0 || all.is_empty();
println!(
"| {n} | {} | {wall:.2} | {:.0} | {:.2} | {:.2} | {:.2} | {failures} | {} |",
all.len(),
all.len() as f64 / wall,
pct(&all, 0.5),
pct(&all, 0.99),
pct(&all, 1.0),
peak_rss_mb().map_or("n/a".to_string(), |m| format!("{m:.0}")),
);
}
if failed {
std::process::exit(1);
}
}