use std::path::{Path, PathBuf};
use anyhow::Result;
mod discovery;
mod markdown;
mod model;
#[derive(Debug, Clone, Copy)]
pub struct ChangeThresholds {
improvement_ratio: f64,
strong_improvement_ratio: f64,
regression_ratio: f64,
}
impl ChangeThresholds {
pub fn improvement_ratio(mut self, ratio: f64) -> Self {
self.improvement_ratio = ratio;
self
}
pub fn strong_improvement_ratio(mut self, ratio: f64) -> Self {
self.strong_improvement_ratio = ratio;
self
}
pub fn regression_ratio(mut self, ratio: f64) -> Self {
self.regression_ratio = ratio;
self
}
fn validate(self) -> Result<()> {
if !self.improvement_ratio.is_finite() || self.improvement_ratio < 1.0 {
anyhow::bail!("improvement ratio must be finite and at least 1.0");
}
if !self.strong_improvement_ratio.is_finite()
|| self.strong_improvement_ratio < self.improvement_ratio
{
anyhow::bail!(
"strong improvement ratio must be finite and at least the improvement ratio"
);
}
if !self.regression_ratio.is_finite()
|| self.regression_ratio <= 0.0
|| self.regression_ratio > 1.0
{
anyhow::bail!("regression ratio must be finite, positive, and at most 1.0");
}
Ok(())
}
}
impl Default for ChangeThresholds {
fn default() -> Self {
Self {
improvement_ratio: 1.1,
strong_improvement_ratio: 1.8,
regression_ratio: 0.9,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub title: String,
pub collapsible: bool,
pub baseline: Option<String>,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
title: "Benchmarks".to_string(),
collapsible: false,
baseline: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Renderer {
criterion_dir: PathBuf,
baseline_root: PathBuf,
candidate: String,
baseline: Option<String>,
included_entries: Vec<String>,
title: String,
collapsible: bool,
change_thresholds: ChangeThresholds,
summary_limit: usize,
}
impl Renderer {
pub fn new(criterion_dir: impl AsRef<Path>) -> Self {
let criterion_dir = criterion_dir.as_ref().to_path_buf();
Self {
baseline_root: criterion_dir.clone(),
criterion_dir,
candidate: "new".to_string(),
baseline: None,
included_entries: Vec::new(),
title: "Benchmarks".to_string(),
collapsible: false,
change_thresholds: ChangeThresholds::default(),
summary_limit: 3,
}
}
pub fn candidate(mut self, candidate: impl AsRef<str>) -> Self {
self.candidate = candidate.as_ref().to_string();
self
}
pub fn baseline(mut self, baseline: impl AsRef<str>) -> Self {
self.baseline = Some(baseline.as_ref().to_string());
self
}
pub fn baseline_root(mut self, baseline_root: impl AsRef<Path>) -> Self {
self.baseline_root = baseline_root.as_ref().to_path_buf();
self
}
pub fn benchmark(mut self, entry: impl AsRef<str>) -> Self {
self.included_entries.push(entry.as_ref().to_string());
self
}
pub fn benchmarks(mut self, entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
self.included_entries
.extend(entries.into_iter().map(|entry| entry.as_ref().to_string()));
self
}
pub fn title(mut self, title: impl AsRef<str>) -> Self {
self.title = title.as_ref().to_string();
self
}
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = collapsible;
self
}
pub fn change_thresholds(mut self, thresholds: ChangeThresholds) -> Self {
self.change_thresholds = thresholds;
self
}
pub fn summary_limit(mut self, limit: usize) -> Self {
self.summary_limit = limit;
self
}
pub fn render(&self) -> Result<String> {
self.change_thresholds.validate()?;
let mut entries = discovery::discover_benchmarks(
&self.criterion_dir,
&self.candidate,
&self.baseline_root,
self.baseline.as_deref(),
)?;
if !self.included_entries.is_empty() {
entries.retain(|entry| self.included_entries.contains(&entry.full_id));
}
if entries.is_empty() {
anyhow::bail!(
"No benchmark results found in {}",
self.criterion_dir.display()
);
}
let summary =
markdown::compute_summary(&entries, self.change_thresholds, self.summary_limit);
let body = markdown::format_table(
&entries,
&self.title,
self.collapsible,
self.change_thresholds,
&summary,
);
if !self.collapsible {
return Ok(body);
}
let headline = summary
.headline()
.map(|headline| format!(" ({headline})"))
.unwrap_or_default();
Ok(format!(
"<details>\n<summary>{}{headline}</summary>\n\n{body}\n</details>\n",
self.title
))
}
}
pub fn render(
criterion_dir: impl AsRef<Path>,
allowlist: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<String> {
Renderer::new(criterion_dir).benchmarks(allowlist).render()
}
pub fn render_with_options(
criterion_dir: impl AsRef<Path>,
allowlist: impl IntoIterator<Item = impl AsRef<str>>,
options: &RenderOptions,
) -> Result<String> {
let mut renderer = Renderer::new(criterion_dir)
.benchmarks(allowlist)
.title(&options.title)
.collapsible(options.collapsible);
if let Some(baseline) = &options.baseline {
renderer = renderer.baseline(baseline);
}
renderer.render()
}