pub mod recipe;
pub mod util;
pub use recipe::{benchmark, BenchParams, Recipe};
pub use util::Lcg;
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::io::{self, Write};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
pub struct Stage {
name: String,
samples: Vec<u64>,
}
impl Stage {
fn new(name: &str, capacity: usize) -> Self {
Self {
name: name.to_string(),
samples: Vec::with_capacity(capacity),
}
}
pub fn record(&mut self, ns: u64) {
self.samples.push(ns);
}
pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
let t0 = Instant::now();
let r = f();
self.samples.push(t0.elapsed().as_nanos() as u64);
r
}
pub fn name(&self) -> &str {
&self.name
}
pub fn samples(&self) -> &[u64] {
&self.samples
}
}
pub struct PerfHarness {
workload: String,
lang: String,
inputs: BTreeMap<String, String>,
meta: BTreeMap<String, String>,
stages: Vec<Stage>,
}
impl PerfHarness {
pub fn new(workload: &str, lang: &str) -> Self {
Self {
workload: workload.to_string(),
lang: lang.to_string(),
inputs: BTreeMap::new(),
meta: BTreeMap::new(),
stages: Vec::new(),
}
}
pub fn input(&mut self, key: &str, value: &str) -> &mut Self {
self.inputs.insert(key.to_string(), value.to_string());
self
}
pub fn meta(&mut self, key: &str, value: &str) -> &mut Self {
self.meta.insert(key.to_string(), value.to_string());
self
}
pub fn stage(&mut self, name: &str, capacity: usize) -> &mut Stage {
self.stages.push(Stage::new(name, capacity));
self.stages.last_mut().unwrap()
}
pub fn stage_mut(&mut self, name: &str) -> Option<&mut Stage> {
self.stages.iter_mut().find(|s| s.name == name)
}
pub fn write_json<W: Write>(&self, out: &mut W) -> io::Result<()> {
let mut s = String::with_capacity(64 * 1024);
s.push('{');
json_kv_str(&mut s, "workload", &self.workload);
s.push(',');
json_kv_str(&mut s, "lang", &self.lang);
s.push(',');
json_kv_str(&mut s, "timestamp", &iso8601_now());
s.push(',');
s.push_str("\"inputs\":");
json_map(&mut s, &self.inputs);
s.push(',');
s.push_str("\"meta\":");
json_map(&mut s, &self.meta);
s.push(',');
s.push_str("\"stages\":{");
for (i, stage) in self.stages.iter().enumerate() {
if i > 0 {
s.push(',');
}
json_str(&mut s, &stage.name);
s.push(':');
json_stage(&mut s, &stage.samples);
}
s.push_str("}}");
out.write_all(s.as_bytes())?;
out.write_all(b"\n")?;
Ok(())
}
pub fn discard_stage(&mut self, name: &str) {
self.stages.retain(|s| s.name != name);
}
}
fn json_str(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
fn json_kv_str(out: &mut String, k: &str, v: &str) {
json_str(out, k);
out.push(':');
json_str(out, v);
}
fn json_map(out: &mut String, m: &BTreeMap<String, String>) {
out.push('{');
for (i, (k, v)) in m.iter().enumerate() {
if i > 0 {
out.push(',');
}
json_kv_str(out, k, v);
}
out.push('}');
}
fn json_stage(out: &mut String, samples: &[u64]) {
let mut sorted = samples.to_vec();
sorted.sort_unstable();
let n = sorted.len();
let p = |q: f64| -> u64 {
if n == 0 {
return 0;
}
let idx = ((q * n as f64) as usize).min(n - 1);
sorted[idx]
};
let max = sorted.last().copied().unwrap_or(0);
let mean = if n == 0 {
0
} else {
sorted.iter().sum::<u64>() / n as u64
};
out.push('{');
let _ = write!(out, "\"count\":{},", n);
let _ = write!(out, "\"p50_ns\":{},", p(0.50));
let _ = write!(out, "\"p99_ns\":{},", p(0.99));
let _ = write!(out, "\"p999_ns\":{},", p(0.999));
let _ = write!(out, "\"max_ns\":{},", max);
let _ = write!(out, "\"mean_ns\":{},", mean);
out.push_str("\"samples_ns\":[");
let step = (n / 500).max(1);
let mut first = true;
for i in (0..n).step_by(step) {
if !first {
out.push(',');
}
first = false;
let _ = write!(out, "{}", samples[i]);
}
out.push_str("]}");
}
fn iso8601_now() -> String {
let d = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs() as i64;
let mut year = 1970i64;
let mut days = secs / 86_400;
let rem = secs % 86_400;
let hour = rem / 3600;
let minute = (rem % 3600) / 60;
let second = rem % 60;
while days >= year_days(year) {
days -= year_days(year);
year += 1;
}
let mut month = 1u32;
for m in 1..=12 {
let dm = month_days(year, m);
if days < dm as i64 {
month = m;
break;
}
days -= dm as i64;
}
let day = (days + 1) as u32;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hour, minute, second
)
}
fn year_days(y: i64) -> i64 {
if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
366
} else {
365
}
}
fn month_days(y: i64, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
29
} else {
28
}
}
_ => 0,
}
}
pub fn read_stdin_kv() -> BTreeMap<String, String> {
use std::io::BufRead;
let mut m = BTreeMap::new();
let stdin = io::stdin();
for line in stdin.lock().lines().map_while(Result::ok) {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
m.insert(k.trim().to_string(), v.trim().to_string());
}
}
m
}