use std::fmt::Write as _;
use serde::{Deserialize, Serialize};
const BASELINE: &str = "arity";
#[derive(Deserialize)]
struct Benchmarks {
meta: Meta,
sections: Vec<Section>,
}
#[derive(Deserialize)]
struct Meta {
generated_at: String,
host: Host,
backend: String,
min_runs: u32,
tools: std::collections::BTreeMap<String, Tool>,
}
#[derive(Deserialize)]
struct Host {
os: String,
arch: String,
cpu: String,
}
#[derive(Deserialize)]
struct Tool {
version: String,
}
#[derive(Deserialize)]
struct Section {
title: String,
charts: Vec<Chart>,
}
#[derive(Deserialize)]
struct Chart {
title: String,
caption: String,
documents: Vec<Document>,
results: Vec<BenchResult>,
}
#[derive(Deserialize)]
struct Document {
id: String,
name: String,
size_bytes: u64,
lines: u64,
}
#[derive(Deserialize)]
struct BenchResult {
document: String,
tool: String,
mean_ms: f64,
stddev_ms: Option<f64>,
min_ms: Option<f64>,
max_ms: Option<f64>,
}
#[derive(Serialize)]
struct ChartPoint {
document: String,
tool: String,
mean_ms: f64,
ratio: f64,
ratio_label: String,
stddev_ms: Option<f64>,
min_ms: Option<f64>,
max_ms: Option<f64>,
}
pub fn render_partials(json: Option<&str>) -> (String, String) {
match json.and_then(|s| serde_json::from_str::<Benchmarks>(s).ok()) {
Some(b) => (render_meta(&b.meta), render_body(&b)),
None => {
let note = "_Benchmark data unavailable \
(`benches/benchmark_results.json` missing or unreadable; \
run `task bench`)._\n"
.to_string();
(note.clone(), note)
}
}
}
fn render_meta(meta: &Meta) -> String {
let mut out = String::new();
let mut names: Vec<&String> = meta.tools.keys().collect();
names.sort_by_key(|n| (*n != BASELINE, (*n).clone()));
for name in names {
let version = &meta.tools[name].version;
let _ = writeln!(out, "- **{name}**: `{version}`");
}
let _ = writeln!(
out,
"- **backend**: {} (min runs: {})",
meta.backend, meta.min_runs
);
let _ = writeln!(
out,
"- **host**: {}/{}, {}",
meta.host.os, meta.host.arch, meta.host.cpu
);
let _ = writeln!(out, "- **generated**: {}", meta.generated_at);
out
}
fn render_body(b: &Benchmarks) -> String {
let mut out = String::new();
for section in &b.sections {
let charts: Vec<&Chart> = section
.charts
.iter()
.filter(|c| !c.results.is_empty())
.collect();
if charts.is_empty() {
continue;
}
let _ = writeln!(out, "### {}\n", section.title);
for chart in charts {
let _ = writeln!(out, "#### {}\n", chart.title);
out.push_str(&render_chart(chart));
out.push('\n');
}
}
if out.is_empty() {
out.push_str("_No benchmark results recorded._\n");
}
out
}
fn render_chart(chart: &Chart) -> String {
let points = chart_points(chart);
let data_json = serde_json::to_string(&points).unwrap_or_else(|_| "[]".to_string());
let mut out = String::new();
out.push_str("<div class=\"bench-chart-block\">\n");
out.push_str("<figure class=\"bench-figure\">\n");
out.push_str("<div class=\"bench-chart\"></div>\n");
out.push_str("<script type=\"application/json\" class=\"bench-data\">");
out.push_str(&data_json);
out.push_str("</script>\n");
let _ = writeln!(out, "<figcaption>{}</figcaption>", esc(&chart.caption));
out.push_str("</figure>\n");
out.push_str(
"<noscript>Enable JavaScript for the interactive chart; \
the data table below has the same numbers.</noscript>\n",
);
out.push_str("<details class=\"bench-table\">\n<summary>Data table</summary>\n");
out.push_str(&render_chart_tables_html(chart));
out.push_str("</details>\n");
out.push_str("</div>\n");
out
}
fn chart_points(chart: &Chart) -> Vec<ChartPoint> {
let mut points = Vec::new();
for doc in &chart.documents {
let base = baseline_mean(chart, &doc.id);
let Some(base) = base.filter(|&b| b > 0.0) else {
continue;
};
for r in chart.results.iter().filter(|r| r.document == doc.id) {
let ratio_label = if r.tool == BASELINE {
"baseline".to_string()
} else {
relative_cell(r.mean_ms, Some(base))
};
points.push(ChartPoint {
document: doc.name.clone(),
tool: r.tool.clone(),
mean_ms: r.mean_ms,
ratio: r.mean_ms / base,
ratio_label,
stddev_ms: r.stddev_ms,
min_ms: r.min_ms,
max_ms: r.max_ms,
});
}
}
points
}
fn render_chart_tables_html(chart: &Chart) -> String {
let order = tool_order(&chart.results);
let mut out = String::new();
for doc in &chart.documents {
let base = baseline_mean(chart, &doc.id);
let _ = writeln!(
out,
"<h5>{} ({} bytes, {} lines)</h5>",
esc(&doc.name),
doc.size_bytes,
doc.lines
);
out.push_str(
"<table>\n<thead><tr><th>Tool</th><th>Mean (ms)</th>\
<th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>\n<tbody>\n",
);
for tool in &order {
let Some(r) = chart
.results
.iter()
.find(|r| r.document == doc.id && &r.tool == tool)
else {
continue;
};
let relative = if r.tool == BASELINE {
"baseline".to_string()
} else {
relative_cell(r.mean_ms, base)
};
let _ = writeln!(
out,
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
esc(&r.tool),
fmt_ms(Some(r.mean_ms)),
fmt_ms(r.min_ms),
fmt_ms(r.max_ms),
esc(&relative),
);
}
out.push_str("</tbody>\n</table>\n");
}
out
}
fn tool_order(results: &[BenchResult]) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut order = Vec::new();
for r in results {
if seen.insert(r.tool.clone()) {
order.push(r.tool.clone());
}
}
order
}
fn baseline_mean(chart: &Chart, doc_id: &str) -> Option<f64> {
chart
.results
.iter()
.find(|r| r.document == doc_id && r.tool == BASELINE)
.map(|r| r.mean_ms)
}
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn fmt_ms(v: Option<f64>) -> String {
match v {
Some(x) => format!("{x:.4}"),
None => "—".to_string(),
}
}
fn relative_cell(tool_mean: f64, base: Option<f64>) -> String {
match base {
Some(b) if b > 0.0 && tool_mean > 0.0 => {
let r = tool_mean / b;
if r >= 1.0 {
format!("{r:.1}x slower")
} else {
format!("{:.1}x faster", 1.0 / r)
}
}
_ => "—".to_string(),
}
}