use crate::{Config, Reason};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Environment {
pub machine: String,
pub os: String,
pub arch: String,
pub date: String,
pub digest: Option<String>,
pub rerun: String,
}
impl Environment {
pub fn detect(rerun: impl Into<String>, digest: Option<String>) -> Self {
Environment {
machine: machine(),
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
date: today_utc(),
digest,
rerun: clean(&rerun.into(), true),
}
}
pub fn comparable_key(&self) -> String {
format!("{} | {} | {}", self.machine, self.os, self.arch)
}
fn to_config(&self) -> Config {
let mut c = Config::new()
.with("arch", &self.arch)
.with("date", &self.date)
.with("machine", clean(&self.machine, false))
.with("os", &self.os);
if let Some(d) = &self.digest {
c = c.with("digest", clean(d, false));
}
c
}
}
fn clean(s: &str, free_text: bool) -> String {
s.chars()
.map(|c| match c {
'\t' | '\n' | '\r' => ' ',
';' | '=' if !free_text => ' ',
c => c,
})
.collect::<String>()
.trim()
.to_string()
}
fn machine() -> String {
let cores = std::thread::available_parallelism().map_or(0, std::num::NonZero::get);
let model = std::env::var("PROCESSOR_IDENTIFIER")
.ok()
.or_else(|| {
std::fs::read_to_string("/proc/cpuinfo").ok().and_then(|t| {
t.lines()
.find(|l| l.starts_with("model name"))
.and_then(|l| l.split_once(':'))
.map(|(_, v)| v.trim().to_string())
})
})
.unwrap_or_else(|| "unknown cpu".to_string());
format!("{} x{cores}", clean(&model, false))
}
fn today_utc() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let (y, m, d) = civil_from_days(i64::try_from(secs / 86_400).unwrap_or(0));
format!("{y:04}-{m:02}-{d:02}")
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = u32::try_from(doy - (153 * mp + 2) / 5 + 1).unwrap_or(1);
let m = u32::try_from(if mp < 10 { mp + 3 } else { mp - 9 }).unwrap_or(1);
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stats {
pub n: u32,
pub p50_ns: u64,
pub p95_ns: u64,
pub p99_ns: u64,
pub max_ns: u64,
pub ops_per_sec: u64,
}
impl Stats {
pub fn from_samples(mut us: Vec<u64>) -> Option<Self> {
if us.is_empty() {
return None;
}
us.sort_unstable();
let n = us.len();
let at = |p: usize| us[(n * p).div_ceil(100).saturating_sub(1).min(n - 1)];
let total: u64 = us.iter().sum();
Some(Stats {
n: u32::try_from(n).unwrap_or(u32::MAX),
p50_ns: at(50),
p95_ns: at(95),
p99_ns: at(99),
max_ns: us[n - 1],
ops_per_sec: u64::try_from(n)
.unwrap_or(u64::MAX)
.saturating_mul(1_000_000_000)
.checked_div(total)
.unwrap_or(0),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Measured(Stats),
Excluded {
leaf: String,
reason: Reason,
note: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpeedRow {
pub workload: String,
pub backend: String,
pub version: String,
pub config: Config,
pub env: Environment,
pub outcome: Outcome,
}
const HEADER: &str = "workload\tbackend\tversion\tconfig\tstatus\tn\tp50_ns\tp95_ns\tp99_ns\tmax_ns\tops_per_sec\tnote\tenv\trerun";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpeedParseError(pub String);
impl fmt::Display for SpeedParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "speed file: {}", self.0)
}
}
impl std::error::Error for SpeedParseError {}
impl SpeedRow {
fn to_line(&self) -> String {
let (status, nums, note) = match &self.outcome {
Outcome::Measured(s) => (
"measured",
[
s.n.to_string(),
s.p50_ns.to_string(),
s.p95_ns.to_string(),
s.p99_ns.to_string(),
s.max_ns.to_string(),
s.ops_per_sec.to_string(),
],
String::new(),
),
Outcome::Excluded { leaf, reason, note } => (
"excluded",
Default::default(),
format!("{leaf}: {}: {}", reason.as_str(), clean(note, true)),
),
};
let mut cols: Vec<String> = vec![
self.workload.clone(),
self.backend.clone(),
self.version.clone(),
self.config.to_string(),
status.to_string(),
];
cols.extend(nums);
cols.push(note);
cols.push(self.env.to_config().to_string());
cols.push(self.env.rerun.clone());
cols.join("\t")
}
fn from_line(line: &str) -> Result<Self, SpeedParseError> {
let c: Vec<&str> = line.split('\t').collect();
if c.len() != 14 {
return Err(SpeedParseError(format!(
"{} columns, want 14: {line:?}",
c.len()
)));
}
let bad = |what: &str| SpeedParseError(format!("{what} in {line:?}"));
let env_cfg = Config::parse(c[12]).map_err(|e| SpeedParseError(e.to_string()))?;
let get = |k: &str| env_cfg.get(k).map(str::to_string).ok_or_else(|| bad(k));
let env = Environment {
machine: get("machine")?,
os: get("os")?,
arch: get("arch")?,
date: get("date")?,
digest: env_cfg.get("digest").map(str::to_string),
rerun: c[13].to_string(),
};
let num = |i: usize| c[i].parse::<u64>().map_err(|_| bad("number"));
let outcome = match c[4] {
"measured" => Outcome::Measured(Stats {
n: u32::try_from(num(5)?).map_err(|_| bad("n"))?,
p50_ns: num(6)?,
p95_ns: num(7)?,
p99_ns: num(8)?,
max_ns: num(9)?,
ops_per_sec: num(10)?,
}),
"excluded" => {
let mut parts = c[11].splitn(3, ": ");
let (leaf, reason, note) = (
parts.next().unwrap_or(""),
parts.next().unwrap_or(""),
parts.next().unwrap_or(""),
);
let reason = match reason {
"missing" => Reason::Missing,
"wrong" => Reason::Wrong,
"untested" => Reason::Untested,
_ => return Err(bad("reason")),
};
Outcome::Excluded {
leaf: leaf.to_string(),
reason,
note: note.to_string(),
}
}
_ => return Err(bad("status")),
};
Ok(SpeedRow {
workload: c[0].to_string(),
backend: c[1].to_string(),
version: c[2].to_string(),
config: Config::parse(c[3]).map_err(|e| SpeedParseError(e.to_string()))?,
env,
outcome,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SpeedTable {
rows: Vec<SpeedRow>,
}
impl SpeedTable {
pub fn from_rows(rows: Vec<SpeedRow>) -> Self {
SpeedTable { rows }
}
pub fn rows(&self) -> &[SpeedRow] {
&self.rows
}
pub fn merge(&mut self, other: SpeedTable) {
self.rows.extend(other.rows);
}
pub fn to_tsv(&self) -> String {
let mut out = String::from(HEADER);
out.push('\n');
for r in &self.rows {
out.push_str(&r.to_line());
out.push('\n');
}
out
}
pub fn parse(text: &str) -> Result<Self, SpeedParseError> {
let mut lines = text.lines();
if lines.next() != Some(HEADER) {
return Err(SpeedParseError("missing or wrong header".to_string()));
}
let rows = lines
.filter(|l| !l.is_empty())
.map(SpeedRow::from_line)
.collect::<Result<_, _>>()?;
Ok(SpeedTable { rows })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ranked {
pub backend: String,
pub version: String,
pub config: Config,
pub stats: Stats,
pub date: String,
pub rerun: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Left {
pub backend: String,
pub version: String,
pub leaf: String,
pub reason: Reason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comparison {
pub workload: String,
pub environment: String,
pub ranked: Vec<Ranked>,
pub excluded: Vec<Left>,
}
pub fn compare(table: &SpeedTable) -> Vec<Comparison> {
let mut groups: BTreeMap<(String, String), Comparison> = BTreeMap::new();
for r in table.rows() {
let key = (r.workload.clone(), r.env.comparable_key());
let g = groups.entry(key.clone()).or_insert_with(|| Comparison {
workload: key.0.clone(),
environment: key.1.clone(),
ranked: Vec::new(),
excluded: Vec::new(),
});
match &r.outcome {
Outcome::Measured(s) => g.ranked.push(Ranked {
backend: r.backend.clone(),
version: r.version.clone(),
config: r.config.clone(),
stats: *s,
date: r.env.date.clone(),
rerun: r.env.rerun.clone(),
}),
Outcome::Excluded { leaf, reason, .. } => g.excluded.push(Left {
backend: r.backend.clone(),
version: r.version.clone(),
leaf: leaf.clone(),
reason: *reason,
}),
}
}
let mut out: Vec<Comparison> = groups.into_values().collect();
for g in &mut out {
g.ranked.sort_by(|a, b| {
(a.stats.p50_ns, &a.backend, &a.version).cmp(&(b.stats.p50_ns, &b.backend, &b.version))
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn env(machine: &str) -> Environment {
Environment {
machine: machine.to_string(),
os: "linux".to_string(),
arch: "x86_64".to_string(),
date: "2026-10-16".to_string(),
digest: Some("sha256:abc".to_string()),
rerun: "cargo run --example speed".to_string(),
}
}
fn measured(backend: &str, machine: &str, p50: u64) -> SpeedRow {
SpeedRow {
workload: "speed.kv.get".to_string(),
backend: backend.to_string(),
version: "1".to_string(),
config: Config::new(),
env: env(machine),
outcome: Outcome::Measured(Stats::from_samples(vec![p50; 10]).unwrap()),
}
}
#[test]
fn percentiles_are_nearest_rank_and_an_empty_sample_has_none() {
assert!(Stats::from_samples(vec![]).is_none());
let s = Stats::from_samples((1..=100).collect()).unwrap();
assert_eq!(
(s.n, s.p50_ns, s.p95_ns, s.p99_ns, s.max_ns),
(100, 50, 95, 99, 100)
);
let one = Stats::from_samples(vec![7]).unwrap();
assert_eq!((one.p50_ns, one.p99_ns, one.max_ns), (7, 7, 7));
}
#[test]
fn dates_are_civil_utc() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(civil_from_days(19_000), (2022, 1, 8));
assert_eq!(civil_from_days(20_742), (2026, 10, 16));
}
#[test]
fn a_table_round_trips_measured_and_excluded_rows() {
let mut rows = vec![measured("a", "m", 5)];
rows.push(SpeedRow {
outcome: Outcome::Excluded {
leaf: "kv.get".to_string(),
reason: Reason::Wrong,
note: "lost a write".to_string(),
},
..measured("b", "m", 1)
});
let t = SpeedTable::from_rows(rows);
assert_eq!(SpeedTable::parse(&t.to_tsv()).unwrap(), t);
}
#[test]
fn a_malformed_file_is_an_error_not_an_empty_table() {
assert!(SpeedTable::parse("nope").is_err());
assert!(SpeedTable::parse(&format!("{HEADER}\ntoo\tfew")).is_err());
}
#[test]
fn rankings_never_cross_machines_and_excluded_backends_are_listed_not_ranked() {
let mut rows = vec![
measured("slow", "laptop", 900),
measured("fast", "laptop", 20),
measured("slow", "server", 5),
measured("fast", "server", 50),
];
rows.push(SpeedRow {
outcome: Outcome::Excluded {
leaf: "kv.get".to_string(),
reason: Reason::Wrong,
note: "lost a write".to_string(),
},
..measured("broken", "laptop", 1)
});
let cmp = compare(&SpeedTable::from_rows(rows));
assert_eq!(cmp.len(), 2, "one comparison per environment");
let laptop = cmp
.iter()
.find(|c| c.environment.starts_with("laptop"))
.unwrap();
let names: Vec<_> = laptop.ranked.iter().map(|r| r.backend.as_str()).collect();
assert_eq!(names, ["fast", "slow"]);
assert_eq!(laptop.excluded.len(), 1);
assert_eq!(laptop.excluded[0].backend, "broken");
let server = cmp
.iter()
.find(|c| c.environment.starts_with("server"))
.unwrap();
let names: Vec<_> = server.ranked.iter().map(|r| r.backend.as_str()).collect();
assert_eq!(names, ["slow", "fast"]);
}
}