use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use crate::bench::BenchRun;
use crate::introspect::{artifact, callgraph_dwarf, depgraph};
#[derive(Debug, Default)]
pub struct FileReport {
pub path: PathBuf,
pub sections: Vec<String>,
pub changed: bool,
}
pub struct Ctx<'a> {
pub repo_root: &'a Path,
pub workspace_root: &'a Path,
pub run: Option<&'a BenchRun>,
pub history: &'a [BenchRun],
}
impl<'a> Ctx<'a> {
pub fn new(
repo_root: &'a Path,
workspace_root: &'a Path,
run: Option<&'a BenchRun>,
) -> Self {
Ctx { repo_root, workspace_root, run, history: &[] }
}
pub fn with_history(mut self, history: &'a [BenchRun]) -> Self {
self.history = history;
self
}
}
#[derive(Debug)]
struct Marker {
line_start: usize,
line_end: usize,
name: String,
args: HashMap<String, String>,
}
const START: &str = "<!-- nornir:gen:start:";
const END: &str = "<!-- nornir:gen:end:";
fn tokenize_marker(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_q = false;
let mut started = false;
for c in s.chars() {
match c {
'"' => {
in_q = !in_q;
started = true;
}
c if c.is_whitespace() && !in_q => {
if started {
out.push(std::mem::take(&mut cur));
started = false;
}
}
c => {
cur.push(c);
started = true;
}
}
}
if started {
out.push(cur);
}
out
}
fn parse_markers(text: &str) -> Result<Vec<(Marker, usize)>> {
let lines: Vec<&str> = text.split_inclusive('\n').collect();
let mut open: Vec<(usize, String, HashMap<String, String>)> = Vec::new();
let mut out: Vec<(Marker, usize)> = Vec::new();
let mut in_fence = false;
for (i, line) in lines.iter().enumerate() {
let t = line.trim();
if t.starts_with("```") || t.starts_with("~~~") {
in_fence = !in_fence;
continue;
}
if in_fence {
continue;
}
if let Some(rest) = t.strip_prefix(START) {
let body = rest
.strip_suffix("-->")
.ok_or_else(|| anyhow!("malformed start marker at line {}: {t}", i + 1))?
.trim();
let mut parts = tokenize_marker(body).into_iter();
let name = parts
.next()
.ok_or_else(|| anyhow!("missing section name at line {}", i + 1))?;
let mut args = HashMap::new();
for kv in parts {
if let Some((k, v)) = kv.split_once('=') {
args.insert(k.to_string(), v.to_string());
}
}
open.push((i, name, args));
} else if let Some(rest) = t.strip_prefix(END) {
let name = rest
.strip_suffix("-->")
.ok_or_else(|| anyhow!("malformed end marker at line {}: {t}", i + 1))?
.trim()
.to_string();
let (start_i, start_name, args) = open
.pop()
.ok_or_else(|| anyhow!("unmatched end marker `{name}` at line {}", i + 1))?;
if start_name != name {
bail!(
"marker mismatch: start `{start_name}` at line {} vs end `{name}` at line {}",
start_i + 1,
i + 1
);
}
out.push((
Marker {
line_start: start_i,
line_end: i,
name,
args,
},
0,
));
}
}
if let Some((i, name, _)) = open.pop() {
bail!("unclosed marker `{name}` opened at line {}", i + 1);
}
Ok(out)
}
fn render(ctx: &Ctx, m: &Marker) -> Result<String> {
let body = match m.name.as_str() {
"bench" => render_bench(ctx)?,
"bench_history" => render_bench_history(ctx, &m.args)?,
"benches" => render_benches(ctx, &m.args)?,
"bench_chart" => render_bench_chart(ctx, &m.args)?,
"bench_hero" => render_bench_hero(ctx, &m.args)?,
"tests" => render_tests(ctx, &m.args)?,
"depgraph" => render_depgraph(ctx)?,
"symbols" => render_symbols(ctx, &m.args)?,
"callgraph" => render_callgraph(ctx, &m.args)?,
other => bail!("unknown section `{other}`"),
};
let mut out = String::new();
out.push_str("<!-- nornir:gen:start:");
out.push_str(&m.name);
for (k, v) in args_sorted(&m.args) {
out.push(' ');
out.push_str(&k);
out.push('=');
if v.chars().any(char::is_whitespace) {
out.push('"');
out.push_str(&v);
out.push('"');
} else {
out.push_str(&v);
}
}
out.push_str(" -->\n");
out.push_str(body.trim_end());
out.push('\n');
out.push_str("<!-- nornir:gen:end:");
out.push_str(&m.name);
out.push_str(" -->\n");
Ok(out)
}
fn args_sorted(a: &HashMap<String, String>) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = a.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
v.sort_by(|a, b| a.0.cmp(&b.0));
v
}
pub fn assemble_file(path: &Path, ctx: &Ctx) -> Result<FileReport> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
let (new_text, sections) = rewrite(&text, ctx)?;
let changed = new_text != text;
if changed {
std::fs::write(path, &new_text)
.with_context(|| format!("write {}", path.display()))?;
}
Ok(FileReport {
path: path.to_path_buf(),
sections,
changed,
})
}
pub fn check_file(path: &Path, ctx: &Ctx) -> Result<FileReport> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
let (new_text, sections) = rewrite(&text, ctx)?;
if new_text != text {
bail!(
"{} is out of date — run `nornir docs assemble` to regenerate",
path.display()
);
}
Ok(FileReport {
path: path.to_path_buf(),
sections,
changed: false,
})
}
pub fn rewrite_str(text: &str, ctx: &Ctx) -> Result<(String, Vec<String>)> {
rewrite(text, ctx)
}
fn rewrite(text: &str, ctx: &Ctx) -> Result<(String, Vec<String>)> {
let markers = parse_markers(text)?;
if markers.is_empty() {
return Ok((text.to_string(), Vec::new()));
}
let lines: Vec<&str> = text.split_inclusive('\n').collect();
let mut out = String::new();
let mut cursor = 0usize;
let mut sections = Vec::new();
for (m, _) in &markers {
for l in &lines[cursor..m.line_start] {
out.push_str(l);
}
out.push_str(&render(ctx, m)?);
sections.push(m.name.clone());
cursor = m.line_end + 1;
}
for l in &lines[cursor..] {
out.push_str(l);
}
Ok((out, sections))
}
fn bench_header(run: &BenchRun) -> String {
let mut parts = vec![format!("v{}", run.version)];
if !run.machine.is_empty() {
parts.push(run.machine.clone());
}
parts.push(format!("{} cores", run.cores));
if !run.date.is_empty() {
parts.push(run.date.clone());
}
format!("**{}**\n\n", parts.join(" · "))
}
fn render_bench(ctx: &Ctx) -> Result<String> {
let Some(run) = ctx.run else {
return Ok("_(no bench history yet — run a benchmark to populate this section)_\n".to_string());
};
let mut out = String::new();
out.push_str(&bench_header(run));
if run.results.is_empty() {
out.push_str("_(no bench results)_\n");
return Ok(out);
}
out.push_str("| name | metric | value |\n|------|--------|-------|\n");
let mut rows: Vec<(String, String, f64)> = Vec::new();
for r in &run.results {
for (k, v) in &r.metrics {
if let Some(f) = v.as_f64() {
rows.push((r.name.clone(), k.clone(), f));
}
}
}
rows.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
for (n, k, v) in rows {
out.push_str(&format!("| {n} | {k} | {v:.2} |\n"));
}
Ok(out)
}
fn render_bench_history(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
if ctx.history.is_empty() {
return Ok(
"_(no bench history yet — run benchmarks to populate this section)_\n".to_string()
);
}
let limit: usize = args.get("limit").and_then(|s| s.parse().ok()).unwrap_or(20);
let metric = args.get("metric").map(|s| s.as_str());
let mut runs: Vec<&BenchRun> = ctx.history.iter().collect();
runs.sort_by(|a, b| {
let ka = a.timestamp.as_deref().unwrap_or(&a.date);
let kb = b.timestamp.as_deref().unwrap_or(&b.date);
kb.cmp(ka)
});
let metric_label = metric.unwrap_or("best metric");
let mut out = String::new();
out.push_str(&format!(
"| Version | Date | Machine | {metric_label} |\n|---|---|---|---|\n"
));
for run in runs.into_iter().take(limit) {
let date = if run.date.is_empty() { "-" } else { run.date.as_str() };
let machine = if run.machine.is_empty() { "-" } else { run.machine.as_str() };
let val = best_metric(run, metric)
.map(|v| format!("{v:.2}"))
.unwrap_or_else(|| "-".to_string());
out.push_str(&format!("| v{} | {} | {} | {} |\n", run.version, date, machine, val));
}
Ok(out)
}
fn best_metric(run: &BenchRun, metric: Option<&str>) -> Option<f64> {
let mut best: Option<f64> = None;
for r in &run.results {
for (k, v) in &r.metrics {
let Some(f) = v.as_f64() else { continue };
let take = match metric {
Some(name) => k == name || r.name == name,
None => true,
};
if take {
best = Some(best.map_or(f, |b| b.max(f)));
}
}
}
best
}
fn col_rank(col: &str) -> u8 {
if col == "ops_sec" || col.ends_with("_per_sec") {
0
} else if col.starts_with("p50") {
10
} else if col.starts_with("p90") {
11
} else if col.starts_with("p999") {
13
} else if col.starts_with("p99") {
12
} else if col.starts_with("mean") {
20
} else if col.starts_with("min") {
21
} else if col.starts_with("max") {
22
} else {
15
}
}
fn render_benches(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
use crate::bench::{direction_of, unit_of, MetricDirection};
let Some(run) = ctx.run else {
return Ok("_(no bench history yet — run a benchmark to populate this section)_\n".to_string());
};
let overrides = parse_best_overrides(args.get("best").map(|s| s.as_str()));
let bold_enabled = args.get("bold").map(|v| v != "false").unwrap_or(true);
let winner_by_row = matches!(args.get("winner").map(|s| s.as_str()), Some("row" | "rows"));
let filter = args.get("name").map(|s| s.as_str());
let exclude = args.get("exclude").map(|s| s.as_str());
let mut results: Vec<&crate::bench::BenchResult> = run
.results
.iter()
.filter(|r| filter.is_none_or(|p| r.name.contains(p)))
.filter(|r| exclude.is_none_or(|p| !r.name.contains(p)))
.collect();
if results.is_empty() {
return Ok("_(no bench results)_\n".to_string());
}
if let Some(spec) = args.get("compare") {
let picked: Vec<&crate::bench::BenchResult> = spec
.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.filter_map(|t| results.iter().find(|r| r.name.contains(t)).copied())
.collect();
if !picked.is_empty() {
results = picked;
}
}
let mut columns: Vec<String> = Vec::new();
for r in &results {
for k in r.metrics.keys() {
if r.metrics[k].as_f64().is_some() && !columns.contains(k) {
columns.push(k.clone());
}
}
}
if let Some(spec) = args.get("cols") {
let want: Vec<&str> = spec.split(',').map(str::trim).filter(|t| !t.is_empty()).collect();
columns.retain(|c| want.iter().any(|w| *w == c));
columns.sort_by_key(|c| want.iter().position(|w| *w == c).unwrap_or(usize::MAX));
} else if args.contains_key("compare") && results.len() >= 2 {
columns.retain(|c| {
let vals: Vec<f64> = results
.iter()
.filter_map(|r| r.metrics.get(c).and_then(|v| v.as_f64()))
.collect();
!(vals.len() == results.len() && vals.windows(2).all(|w| w[0] == w[1]))
});
columns.sort_by(|a, b| col_rank(a).cmp(&col_rank(b)).then_with(|| a.cmp(b)));
} else {
columns.sort();
}
let mut out = String::new();
out.push_str(&bench_header(run));
out.push_str("| workload |");
for c in &columns {
out.push_str(&format!(" {c} |"));
}
out.push('\n');
out.push_str("|---|");
for _ in &columns {
out.push_str("---|");
}
out.push('\n');
let pick_best = |cells: &[(usize, f64)], dir: MetricDirection| -> Option<usize> {
match dir {
MetricDirection::High => cells.iter().max_by(|a, b| a.1.total_cmp(&b.1)),
MetricDirection::Low => cells.iter().min_by(|a, b| a.1.total_cmp(&b.1)),
MetricDirection::Neutral => None,
}
.map(|(i, _)| *i)
};
let mut bold: std::collections::HashSet<(usize, &String)> = std::collections::HashSet::new();
if bold_enabled && winner_by_row {
for c in &columns {
let dir = direction_of(&overrides, c);
if dir == MetricDirection::Neutral {
continue;
}
let cells: Vec<(usize, f64)> = results
.iter()
.enumerate()
.filter_map(|(i, r)| r.metrics.get(c).and_then(|v| v.as_f64()).map(|f| (i, f)))
.collect();
if cells.len() < 2 {
continue;
}
if let Some(i) = pick_best(&cells, dir) {
bold.insert((i, c));
}
}
} else if bold_enabled {
for (i, r) in results.iter().enumerate() {
let mut groups: HashMap<&'static str, Vec<(usize, f64)>> = HashMap::new();
let mut col_of: HashMap<usize, &String> = HashMap::new();
for (ci, c) in columns.iter().enumerate() {
if let Some(f) = r.metrics.get(c).and_then(|v| v.as_f64()) {
if let Some(u) = unit_of(c) {
groups.entry(u.name).or_default().push((ci, f));
col_of.insert(ci, c);
}
}
}
for cells in groups.values() {
if cells.len() < 2 {
continue;
}
let dir = direction_of(&overrides, col_of[&cells[0].0]);
if let Some(ci) = pick_best(cells, dir) {
bold.insert((i, col_of[&ci]));
}
}
}
}
for (i, r) in results.iter().enumerate() {
out.push_str(&format!("| {} |", r.name));
for c in &columns {
match r.metrics.get(c).and_then(|v| v.as_f64()) {
Some(f) => {
let cell = fmt_metric(f);
if bold.contains(&(i, c)) {
out.push_str(&format!(" **{cell}** |"));
} else {
out.push_str(&format!(" {cell} |"));
}
}
None => out.push_str(" |"),
}
}
out.push('\n');
}
Ok(out)
}
fn render_bench_hero(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
let Some(run) = ctx.run else {
return Ok("_(no bench history yet — run a benchmark to populate this section)_\n".to_string());
};
let name = args.get("name").map(|s| s.as_str()).unwrap_or("");
let (Some(fast_tok), Some(slow_tok)) = (args.get("fast"), args.get("slow")) else {
return Ok("_(bench_hero needs `fast=` and `slow=` backend tokens)_\n".to_string());
};
let unit = args.get("unit").map(|s| s.as_str()).unwrap_or("_s");
let find = |tok: &str| {
run.results
.iter()
.find(|r| r.name.contains(name) && r.name.contains(tok))
};
let (Some(fast), Some(slow)) = (find(fast_tok), find(slow_tok)) else {
return Ok("_(bench_hero: backend rows not found for this run)_\n".to_string());
};
let mut best: Option<(String, f64, f64, f64)> = None;
for (k, v) in fast.metrics.iter() {
let Some(stem) = k.strip_suffix(unit) else { continue };
let is_item =
stem.len() > 1 && stem.starts_with('q') && stem[1..].bytes().all(|b| b.is_ascii_digit());
if !is_item {
continue;
}
let (Some(fv), Some(sv)) = (v.as_f64(), slow.metrics.get(k).and_then(|x| x.as_f64())) else {
continue;
};
if fv <= 0.0 {
continue;
}
let ratio = sv / fv;
if best.as_ref().is_none_or(|b| ratio > b.1) {
best = Some((stem.to_string(), ratio, fv, sv));
}
}
let Some((item, ratio, fv, sv)) = best else {
return Ok("_(bench_hero: the two backends share no per-item metrics)_\n".to_string());
};
let label = args.get("label").map(|s| s.as_str()).unwrap_or("query");
let mut out = String::new();
out.push_str(&bench_header(run));
out.push_str(&format!(
"> **Most dramatic {label} — `{item}`: nornir is {ratio:.1}× faster** \
({:.1} ms vs {:.1} ms).\n\n",
fv * 1000.0,
sv * 1000.0
));
out.push_str(&format!(
"| {label} | {} | {} | speedup |\n|---|---|---|---|\n| `{item}` | **{:.1} ms** | {:.1} ms | **{:.1}×** |\n",
fast.name,
slow.name,
fv * 1000.0,
sv * 1000.0,
ratio
));
Ok(out)
}
fn render_bench_chart(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
use crate::bench::{direction_of, MetricDirection};
let Some(run) = ctx.run else {
return Ok("_(no bench history yet — run a benchmark to populate this section)_\n".to_string());
};
let metric = args
.get("metric")
.map(|s| s.as_str())
.ok_or_else(|| anyhow!("bench_chart requires a `metric=` arg"))?;
let filter = args.get("name").map(|s| s.as_str());
let exclude = args.get("exclude").map(|s| s.as_str());
let mut points: Vec<(String, f64)> = run
.results
.iter()
.filter(|r| filter.is_none_or(|p| r.name.contains(p)))
.filter(|r| exclude.is_none_or(|p| !r.name.contains(p)))
.filter_map(|r| r.metrics.get(metric).and_then(|v| v.as_f64()).map(|f| (r.name.clone(), f)))
.collect();
if points.is_empty() {
return Ok(format!("_(no bench results carry metric `{metric}`)_\n"));
}
let overrides = parse_best_overrides(args.get("best").map(|s| s.as_str()));
let dir = match args.get("dir").map(|s| s.to_ascii_lowercase()) {
Some(d) if d == "high" || d == "max" => MetricDirection::High,
Some(d) if d == "low" || d == "min" => MetricDirection::Low,
_ => direction_of(&overrides, metric),
};
match dir {
MetricDirection::High => points.sort_by(|a, b| b.1.total_cmp(&a.1)),
MetricDirection::Low => points.sort_by(|a, b| a.1.total_cmp(&b.1)),
MetricDirection::Neutral => {}
}
let best_val = match dir {
MetricDirection::High => points.iter().map(|p| p.1).fold(f64::MIN, f64::max),
MetricDirection::Low => points.iter().map(|p| p.1).fold(f64::MAX, f64::min),
MetricDirection::Neutral => f64::NAN,
};
let title = args
.get("title")
.cloned()
.unwrap_or_else(|| format!("{metric} by workload"));
let unit = friendly_unit(metric);
let max_v = points.iter().map(|p| p.1).fold(f64::MIN, f64::max);
let min_pos = points.iter().map(|p| p.1).filter(|v| *v > 0.0).fold(f64::MAX, f64::min);
let log = match args.get("log").map(|s| s.as_str()) {
Some("true") => true,
Some("false") => false,
_ => min_pos.is_finite() && max_v > 0.0 && max_v / min_pos > 100.0,
};
#[cfg(feature = "docs-export")]
{
let bars: Vec<crate::docs::svg::Bar> = points
.iter()
.map(|(label, v)| crate::docs::svg::Bar {
label: label.clone(),
value: *v,
best: dir != MetricDirection::Neutral && *v == best_val,
})
.collect();
let chart = crate::docs::svg::BarChart { title: title.clone(), unit: unit.clone(), bars, log };
let base = args
.get("file")
.cloned()
.unwrap_or_else(|| format!("bench_{}", sanitize_asset(metric)));
let rel = std::path::PathBuf::from(format!(".nornir/assets/{base}.svg"));
let abs = ctx.repo_root.join(&rel);
crate::docs::svg::render_bars_to_file(&chart, &abs)?;
let alt = title.replace(['[', ']'], "");
return Ok(format!("\n", alt, rel.display()));
}
#[cfg(not(feature = "docs-export"))]
{
let _ = (log, best_val);
let mut out = format!("**{title}**\n\n");
let max = points.iter().map(|p| p.1).fold(f64::MIN, f64::max).max(f64::MIN_POSITIVE);
for (label, v) in &points {
let filled = ((v / max).clamp(0.0, 1.0) * 24.0).round() as usize;
let bar: String = "█".repeat(filled);
let star = if dir != MetricDirection::Neutral && *v == best_val { " ★" } else { "" };
let unit_sfx = if unit.is_empty() { String::new() } else { format!(" {unit}") };
out.push_str(&format!("- `{label}` {bar} {}{}{star}\n", fmt_metric(*v), unit_sfx));
}
out.push('\n');
Ok(out)
}
}
fn friendly_unit(metric: &str) -> String {
let m = metric.to_ascii_lowercase();
if m.contains("ops_sec") || m.contains("ops_per_sec") || m.contains("commits_per_sec") {
"ops/sec".into()
} else if m.contains("rows_per_sec") || m.ends_with("_per_sec") {
"/sec".into()
} else if m.ends_with("_us") {
"µs".into()
} else if m.ends_with("_ms") {
"ms".into()
} else if m.ends_with("_ns") {
"ns".into()
} else if m.ends_with("_mbs") || m.contains("mbps") || m.contains("mb_per_sec") {
"MB/s".into()
} else if m.ends_with("_secs") || m == "seconds" {
"s".into()
} else {
String::new()
}
}
#[cfg_attr(not(feature = "docs-export"), allow(dead_code))]
fn sanitize_asset(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
fn render_tests(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
let include_benches = args.get("include_benches").map(|v| v == "true").unwrap_or(false);
let inv = crate::docs::test_inventory::scan_opts(
ctx.repo_root,
args.get("crate").map(|s| s.as_str()),
include_benches,
);
let only = args.get("kind").map(|s| s.as_str());
let mut out = String::new();
let want = |k: &str| only.is_none_or(|o| o == k);
if want("unit") {
out.push_str(&format!("**Unit tests** ({})\n\n", inv.unit_total()));
out.push_str(&file_tests_table(&inv.unit));
out.push('\n');
}
if want("integration") {
out.push_str(&format!("**Integration tests** ({})\n\n", inv.integration_total()));
out.push_str(&file_tests_table(&inv.integration));
out.push('\n');
}
if want("doc") {
out.push_str(&format!("**Doc tests** ({})\n\n", inv.doc_total()));
if inv.doc.is_empty() {
out.push_str("_(none)_\n");
} else {
out.push_str("| file | doc-tests |\n|---|--:|\n");
for f in &inv.doc {
out.push_str(&format!("| `{}` | {} |\n", md_escape(&f.file), f.count));
}
}
}
Ok(out.trim_end().to_string() + "\n")
}
fn file_tests_table(files: &[crate::docs::test_inventory::FileTests]) -> String {
if files.is_empty() {
return "_(none)_\n".to_string();
}
let mut out = String::from("| file | tests |\n|---|---|\n");
for f in files {
let names = f
.tests
.iter()
.map(|t| format!("`{}`", md_escape(t)))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!("| `{}` | {} |\n", md_escape(&f.file), names));
}
out
}
fn parse_best_overrides(arg: Option<&str>) -> HashMap<String, crate::bench::MetricDirection> {
use crate::bench::MetricDirection;
let mut out = HashMap::new();
let Some(s) = arg else { return out };
for pair in s.split(',') {
if let Some((metric, dir)) = pair.split_once(':') {
let d = match dir.trim().to_ascii_lowercase().as_str() {
"high" | "max" | "up" => MetricDirection::High,
"low" | "min" | "down" => MetricDirection::Low,
"neutral" | "none" => MetricDirection::Neutral,
_ => continue,
};
out.insert(metric.trim().to_string(), d);
}
}
out
}
fn fmt_metric(f: f64) -> String {
let a = f.abs();
if a >= 1e6 {
let (scaled, suffix) = if a >= 1e12 {
(f / 1e12, "T")
} else if a >= 1e9 {
(f / 1e9, "B")
} else {
(f / 1e6, "M")
};
let s = format!("{scaled:.2}");
let s = s.trim_end_matches('0').trim_end_matches('.');
return format!("{s}{suffix}");
}
if a >= 1e3 {
return group_thousands(&format!("{}", f.round() as i64));
}
let plain = if f.fract() == 0.0 {
format!("{}", f as i64)
} else {
format!("{f:.2}")
};
group_thousands(&plain)
}
fn group_thousands(s: &str) -> String {
let (sign, rest) = s.strip_prefix('-').map_or(("", s), |r| ("-", r));
let (int_part, frac) = rest.split_once('.').map_or((rest, None), |(i, f)| (i, Some(f)));
let n = int_part.len();
let mut grouped = String::with_capacity(n + n / 3);
for (i, ch) in int_part.char_indices() {
if i > 0 && (n - i) % 3 == 0 {
grouped.push(',');
}
grouped.push(ch);
}
match frac {
Some(f) => format!("{sign}{grouped}.{f}"),
None => format!("{sign}{grouped}"),
}
}
fn render_depgraph(ctx: &Ctx) -> Result<String> {
let g = depgraph::extract(ctx.repo_root)?;
#[cfg(feature = "docs-export")]
{
let rel = std::path::PathBuf::from(".nornir/assets/depgraph.svg");
let abs = ctx.repo_root.join(&rel);
crate::docs::svg::render_to_file(&g, &abs)?;
return Ok(format!(
"\n",
rel.display()
));
}
#[cfg(not(feature = "docs-export"))]
{
let mut out = g.to_mermaid();
if !out.ends_with('\n') {
out.push('\n');
}
Ok(out)
}
}
fn resolve_binary(ctx: &Ctx, raw: &str) -> PathBuf {
let p = PathBuf::from(raw);
if p.is_absolute() {
p
} else if ctx.repo_root.join(&p).exists() {
ctx.repo_root.join(&p)
} else {
ctx.workspace_root.join(&p)
}
}
fn render_symbols(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
let bin_arg = args
.get("binary")
.ok_or_else(|| anyhow!("section `symbols` needs binary=PATH"))?;
let limit: usize = args
.get("limit")
.and_then(|v| v.parse().ok())
.unwrap_or(20);
let binary = resolve_binary(ctx, bin_arg);
let syms = artifact::extract_symbols(&binary, ctx.workspace_root)?;
let mut visible: Vec<&artifact::Symbol> = syms
.iter()
.filter(|s| !s.name_demangled.contains("::{{closure}}"))
.collect();
visible.sort_by(|a, b| b.size_bytes.unwrap_or(0).cmp(&a.size_bytes.unwrap_or(0)));
visible.truncate(limit);
let mut out = String::new();
out.push_str(&format!(
"_top {} symbols by size from `{}`_\n\n",
visible.len(),
bin_arg
));
out.push_str("| symbol | size | file |\n|--------|------|------|\n");
for s in &visible {
let file = if s.file.is_empty() { "?" } else { s.file.as_str() };
let size = s
.size_bytes
.map(|n| n.to_string())
.unwrap_or_else(|| "-".into());
out.push_str(&format!(
"| `{}` | {} | {} |\n",
md_escape(&s.name_demangled),
size,
file
));
}
Ok(out)
}
fn render_callgraph(ctx: &Ctx, args: &HashMap<String, String>) -> Result<String> {
let bin_arg = args
.get("binary")
.ok_or_else(|| anyhow!("section `callgraph` needs binary=PATH"))?;
let limit: usize = args
.get("limit")
.and_then(|v| v.parse().ok())
.unwrap_or(15);
let binary = resolve_binary(ctx, bin_arg);
let edges = callgraph_dwarf::extract_callgraph(&binary, ctx.workspace_root)?;
let mut count: HashMap<String, usize> = HashMap::new();
for e in &edges {
*count.entry(e.callee.clone()).or_default() += 1;
}
let mut ranked: Vec<(String, usize)> = count.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
ranked.truncate(limit);
let mut out = String::new();
out.push_str(&format!(
"_{} inline edges in `{}`; top {} callees:_\n\n",
edges.len(),
bin_arg,
ranked.len()
));
out.push_str("| callee | inline sites |\n|--------|-------------:|\n");
for (n, c) in ranked {
out.push_str(&format!("| `{}` | {} |\n", md_escape(&n), c));
}
Ok(out)
}
fn md_escape(s: &str) -> String {
s.replace('|', "\\|").replace('`', "\\`")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Map, Value};
fn ctx<'a>(root: &'a Path, run: Option<&'a BenchRun>) -> Ctx<'a> {
Ctx::new(root, root, run)
}
fn run_fixture() -> BenchRun {
let mut m: Map<String, Value> = Map::new();
m.insert("ops".to_string(), Value::from(123.45f64));
BenchRun {
date: "2026-05-30".into(),
timestamp: None,
version: "0.1.0".into(),
machine: "test".into(),
cores: 8,
results: vec![crate::bench::BenchResult {
name: "decode".into(),
metrics: m,
}],
tests: vec![],
}
}
#[test]
fn parses_and_rewrites_bench_section() {
let r = run_fixture();
let d = tempfile::tempdir().unwrap();
let p = d.path().join("README.md");
std::fs::write(
&p,
"# x\n\n<!-- nornir:gen:start:bench -->\nstale\n<!-- nornir:gen:end:bench -->\n\ntail\n",
)
.unwrap();
let c = ctx(d.path(), Some(&r));
let rep = assemble_file(&p, &c).unwrap();
assert!(rep.changed);
assert_eq!(rep.sections, vec!["bench".to_string()]);
let txt = std::fs::read_to_string(&p).unwrap();
assert!(txt.contains("v0.1.0"));
assert!(txt.contains("decode"));
assert!(txt.ends_with("tail\n"));
}
#[test]
fn bench_history_renders_rows_newest_first() {
let mk = |date: &str, ver: &str, ops: f64| -> BenchRun {
let mut m: Map<String, Value> = Map::new();
m.insert("ops".into(), Value::from(ops));
BenchRun {
date: date.into(),
timestamp: None,
version: ver.into(),
machine: "tr".into(),
cores: 8,
results: vec![crate::bench::BenchResult { name: "x".into(), metrics: m }],
tests: vec![],
}
};
let history = vec![mk("2026-05-01", "0.1.0", 100.0), mk("2026-06-01", "0.2.0", 200.0)];
let root = Path::new(".");
let c = Ctx::new(root, root, None).with_history(&history);
let text =
"<!-- nornir:gen:start:bench_history -->\n<!-- nornir:gen:end:bench_history -->\n";
let (out, sections) = rewrite_str(text, &c).unwrap();
assert_eq!(sections, vec!["bench_history".to_string()]);
assert!(out.contains("| Version | Date | Machine |"), "header: {out}");
let i2 = out.find("v0.2.0").expect("v0.2.0 present");
let i1 = out.find("v0.1.0").expect("v0.1.0 present");
assert!(i2 < i1, "newest (0.2.0) must come first:\n{out}");
assert!(out.contains("200.00") && out.contains("100.00"), "best-metric values: {out}");
}
#[test]
fn bench_history_empty_is_graceful() {
let root = Path::new(".");
let c = Ctx::new(root, root, None); let text =
"<!-- nornir:gen:start:bench_history -->\n<!-- nornir:gen:end:bench_history -->\n";
let (out, _) = rewrite_str(text, &c).unwrap();
assert!(out.contains("no bench history"), "graceful empty: {out}");
}
fn vs_run() -> BenchRun {
let mut m: Map<String, Value> = Map::new();
m.insert("ljar_mbs".into(), Value::from(2527.0f64));
m.insert("unzip_mbs".into(), Value::from(1404.0f64));
m.insert("speedup_x".into(), Value::from(1.8f64));
BenchRun {
date: "2026-06-05".into(),
timestamp: None,
version: "0.9.0".into(),
machine: "test".into(),
cores: 32,
results: vec![crate::bench::BenchResult { name: "jar_2000".into(), metrics: m }],
tests: vec![],
}
}
#[test]
fn benches_pivots_and_bolds_winner() {
let r = vs_run();
let c = ctx(Path::new("/tmp"), Some(&r));
let out = render_benches(&c, &HashMap::new()).unwrap();
assert!(out.contains("| workload |"), "got: {out}");
assert!(out.contains("ljar_mbs"));
assert!(out.contains("jar_2000"));
assert!(out.contains("**2,527**"), "winner not bolded: {out}");
assert!(!out.contains("**1,404**"), "loser bolded: {out}");
assert!(!out.contains("**1.8"), "neutral bolded: {out}");
}
#[test]
fn benches_exclude_and_bold_false() {
let mut a: Map<String, Value> = Map::new();
a.insert("ljar_mbs".into(), Value::from(1000.0f64));
a.insert("unzip_mbs".into(), Value::from(60.0f64));
let mut b: Map<String, Value> = Map::new();
b.insert("ljar_mbs".into(), Value::from(120.0f64));
b.insert("unzip_mbs".into(), Value::from(60.0f64));
let run = BenchRun {
date: "2026-06-06".into(), timestamp: None, version: "0.9.0".into(),
machine: "tr".into(), cores: 32,
results: vec![
crate::bench::BenchResult { name: "ljar_guava".into(), metrics: a },
crate::bench::BenchResult { name: "ljar_guava_st".into(), metrics: b },
],
tests: vec![],
};
let c = ctx(Path::new("/tmp"), Some(&run));
let mut args = HashMap::new();
args.insert("exclude".to_string(), "_st".to_string());
args.insert("bold".to_string(), "false".to_string());
let out = render_benches(&c, &args).unwrap();
assert!(out.contains("ljar_guava"), "got: {out}");
assert!(!out.contains("ljar_guava_st"), "exclude failed: {out}");
assert!(!out.contains("**1,000**"), "bold=false should suppress cell bolding: {out}");
assert!(out.contains("tr · 32 cores"), "header missing machine: {out}");
}
#[test]
fn benches_best_override_flips_direction() {
let r = vs_run();
let c = ctx(Path::new("/tmp"), Some(&r));
let mut args = HashMap::new();
args.insert("best".to_string(), "ljar_mbs:low,unzip_mbs:low".to_string());
let out = render_benches(&c, &args).unwrap();
assert!(out.contains("**1,404**"), "override not applied: {out}");
assert!(!out.contains("**2,527**"), "override ignored: {out}");
}
fn systems_run() -> BenchRun {
let mk = |name: &str, ops: f64, p50: f64, p99: f64| {
let mut m: Map<String, Value> = Map::new();
m.insert("ops_sec".into(), Value::from(ops));
m.insert("p50_us".into(), Value::from(p50));
m.insert("p99_us".into(), Value::from(p99));
crate::bench::BenchResult { name: name.into(), metrics: m }
};
BenchRun {
date: "2026-06-08".into(),
timestamp: None,
version: "0.4.0".into(),
machine: "oden".into(),
cores: 32,
results: vec![
mk("nessie_table_exists", 5270.0, 156.51, 882.0),
mk("nornir_embedded_table_exists", 2345144.0, 0.30, 0.40),
mk("polaris_table_exists", 3282.0, 291.91, 457.0),
],
tests: vec![],
}
}
#[test]
fn benches_winner_by_row_bolds_best_system_per_column() {
let r = systems_run();
let c = ctx(Path::new("/tmp"), Some(&r));
let mut args = HashMap::new();
args.insert("winner".to_string(), "row".to_string());
let out = render_benches(&c, &args).unwrap();
assert!(out.contains("**2.35M**"), "ops/sec winner not bolded: {out}");
assert!(out.contains("**0.3**") || out.contains("**0.30**"), "p50 winner not bolded: {out}");
assert!(out.contains("**0.4**") || out.contains("**0.40**"), "p99 winner not bolded: {out}");
assert!(!out.contains("**156.51**"), "nessie p50 wrongly bolded: {out}");
assert!(!out.contains("**291.91**"), "polaris p50 wrongly bolded: {out}");
}
#[test]
fn fmt_metric_humanizes_and_rounds() {
assert_eq!(fmt_metric(3_390_444.67), "3.39M");
assert_eq!(fmt_metric(2_999_875.48), "3M");
assert_eq!(fmt_metric(1_071_379.54), "1.07M");
assert_eq!(fmt_metric(8_869.62), "8,870");
assert_eq!(fmt_metric(12_809.87), "12,810");
assert_eq!(fmt_metric(3_591.38), "3,591");
assert_eq!(fmt_metric(842.14), "842.14");
assert_eq!(fmt_metric(0.32), "0.32");
assert_eq!(fmt_metric(9.0), "9");
}
#[test]
fn benches_default_axis_does_not_compare_across_rows() {
let r = systems_run();
let c = ctx(Path::new("/tmp"), Some(&r));
let out = render_benches(&c, &HashMap::new()).unwrap();
assert!(!out.contains("**2.35M**"), "col-axis should not bold across rows: {out}");
}
#[test]
fn bench_chart_text_fallback_marks_winner() {
let r = systems_run();
let d = tempfile::tempdir().unwrap();
let c = ctx(d.path(), Some(&r));
let mut args = HashMap::new();
args.insert("metric".to_string(), "ops_sec".to_string());
args.insert("name".to_string(), "table_exists".to_string());
let out = render_bench_chart(&c, &args).unwrap();
#[cfg(feature = "docs-export")]
assert!(out.contains("![") && out.contains(".svg"), "expected image link: {out}");
#[cfg(not(feature = "docs-export"))]
{
assert!(out.contains('★'), "winner star missing: {out}");
assert!(out.contains("nornir_embedded_table_exists"), "label missing: {out}");
}
}
#[test]
fn check_errors_on_drift() {
let r = run_fixture();
let d = tempfile::tempdir().unwrap();
let p = d.path().join("README.md");
std::fs::write(
&p,
"<!-- nornir:gen:start:bench -->\nstale\n<!-- nornir:gen:end:bench -->\n",
)
.unwrap();
let c = ctx(d.path(), Some(&r));
assert!(check_file(&p, &c).is_err());
}
#[test]
fn idempotent_after_assemble() {
let r = run_fixture();
let d = tempfile::tempdir().unwrap();
let p = d.path().join("README.md");
std::fs::write(
&p,
"<!-- nornir:gen:start:bench -->\n<!-- nornir:gen:end:bench -->\n",
)
.unwrap();
let c = ctx(d.path(), Some(&r));
assemble_file(&p, &c).unwrap();
let r1 = check_file(&p, &c).unwrap();
assert!(!r1.changed);
}
#[test]
fn unknown_section_errors() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("README.md");
std::fs::write(
&p,
"<!-- nornir:gen:start:nope -->\n<!-- nornir:gen:end:nope -->\n",
)
.unwrap();
let c = ctx(d.path(), None);
assert!(assemble_file(&p, &c).is_err());
}
#[test]
fn unmatched_marker_errors() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("README.md");
std::fs::write(&p, "<!-- nornir:gen:start:bench -->\nno end\n").unwrap();
let c = ctx(d.path(), None);
assert!(assemble_file(&p, &c).is_err());
}
}