criterion_markdown/
lib.rs1use std::path::{Path, PathBuf};
30
31use anyhow::Result;
32
33mod discovery;
34mod markdown;
35mod model;
36
37#[derive(Debug, Clone, Copy)]
42pub struct ChangeThresholds {
43 improvement_ratio: f64,
44 strong_improvement_ratio: f64,
45 regression_ratio: f64,
46}
47
48impl ChangeThresholds {
49 pub fn improvement_ratio(mut self, ratio: f64) -> Self {
51 self.improvement_ratio = ratio;
52 self
53 }
54
55 pub fn strong_improvement_ratio(mut self, ratio: f64) -> Self {
57 self.strong_improvement_ratio = ratio;
58 self
59 }
60
61 pub fn regression_ratio(mut self, ratio: f64) -> Self {
63 self.regression_ratio = ratio;
64 self
65 }
66
67 fn validate(self) -> Result<()> {
68 if !self.improvement_ratio.is_finite() || self.improvement_ratio < 1.0 {
69 anyhow::bail!("improvement ratio must be finite and at least 1.0");
70 }
71 if !self.strong_improvement_ratio.is_finite()
72 || self.strong_improvement_ratio < self.improvement_ratio
73 {
74 anyhow::bail!(
75 "strong improvement ratio must be finite and at least the improvement ratio"
76 );
77 }
78 if !self.regression_ratio.is_finite()
79 || self.regression_ratio <= 0.0
80 || self.regression_ratio > 1.0
81 {
82 anyhow::bail!("regression ratio must be finite, positive, and at most 1.0");
83 }
84 Ok(())
85 }
86}
87
88impl Default for ChangeThresholds {
89 fn default() -> Self {
90 Self {
91 improvement_ratio: 1.1,
92 strong_improvement_ratio: 1.8,
93 regression_ratio: 0.9,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct RenderOptions {
101 pub title: String,
103 pub collapsible: bool,
105 pub baseline: String,
109}
110
111impl Default for RenderOptions {
112 fn default() -> Self {
113 Self {
114 title: "Benchmarks".to_string(),
115 collapsible: false,
116 baseline: "base".to_string(),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct Renderer {
124 criterion_dir: PathBuf,
125 baseline_root: PathBuf,
126 candidate: String,
127 baseline: String,
128 included_entries: Vec<String>,
129 title: String,
130 collapsible: bool,
131 change_thresholds: ChangeThresholds,
132 summary_limit: usize,
133}
134
135impl Renderer {
136 pub fn new(criterion_dir: impl AsRef<Path>) -> Self {
138 let criterion_dir = criterion_dir.as_ref().to_path_buf();
139 Self {
140 baseline_root: criterion_dir.clone(),
141 criterion_dir,
142 candidate: "new".to_string(),
143 baseline: "base".to_string(),
144 included_entries: Vec::new(),
145 title: "Benchmarks".to_string(),
146 collapsible: false,
147 change_thresholds: ChangeThresholds::default(),
148 summary_limit: 3,
149 }
150 }
151
152 pub fn candidate(mut self, candidate: impl AsRef<str>) -> Self {
154 self.candidate = candidate.as_ref().to_string();
155 self
156 }
157
158 pub fn baseline(mut self, baseline: impl AsRef<str>) -> Self {
160 self.baseline = baseline.as_ref().to_string();
161 self
162 }
163
164 pub fn baseline_root(mut self, baseline_root: impl AsRef<Path>) -> Self {
168 self.baseline_root = baseline_root.as_ref().to_path_buf();
169 self
170 }
171
172 pub fn benchmark(mut self, entry: impl AsRef<str>) -> Self {
176 self.included_entries.push(entry.as_ref().to_string());
177 self
178 }
179
180 pub fn benchmarks(mut self, entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
185 self.included_entries
186 .extend(entries.into_iter().map(|entry| entry.as_ref().to_string()));
187 self
188 }
189
190 pub fn title(mut self, title: impl AsRef<str>) -> Self {
192 self.title = title.as_ref().to_string();
193 self
194 }
195
196 pub fn collapsible(mut self, collapsible: bool) -> Self {
198 self.collapsible = collapsible;
199 self
200 }
201
202 pub fn change_thresholds(mut self, thresholds: ChangeThresholds) -> Self {
204 self.change_thresholds = thresholds;
205 self
206 }
207
208 pub fn summary_limit(mut self, limit: usize) -> Self {
211 self.summary_limit = limit;
212 self
213 }
214
215 pub fn render(&self) -> Result<String> {
217 self.change_thresholds.validate()?;
218 let mut entries = discovery::discover_benchmarks(
219 &self.criterion_dir,
220 &self.candidate,
221 &self.baseline_root,
222 &self.baseline,
223 )?;
224 if !self.included_entries.is_empty() {
225 entries.retain(|entry| self.included_entries.contains(&entry.full_id));
226 }
227 if entries.is_empty() {
228 anyhow::bail!(
229 "No benchmark results found in {}",
230 self.criterion_dir.display()
231 );
232 }
233 let summary =
234 markdown::compute_summary(&entries, self.change_thresholds, self.summary_limit);
235 let body = markdown::format_table(
236 &entries,
237 &self.title,
238 self.collapsible,
239 self.change_thresholds,
240 &summary,
241 );
242 if !self.collapsible {
243 return Ok(body);
244 }
245 let headline = summary
246 .headline()
247 .map(|headline| format!(" ({headline})"))
248 .unwrap_or_default();
249 Ok(format!(
250 "<details>\n<summary>{}{headline}</summary>\n\n{body}\n</details>\n",
251 self.title
252 ))
253 }
254}
255
256pub fn render(
263 criterion_dir: impl AsRef<Path>,
264 allowlist: impl IntoIterator<Item = impl AsRef<str>>,
265) -> Result<String> {
266 Renderer::new(criterion_dir).benchmarks(allowlist).render()
267}
268
269pub fn render_with_options(
271 criterion_dir: impl AsRef<Path>,
272 allowlist: impl IntoIterator<Item = impl AsRef<str>>,
273 options: &RenderOptions,
274) -> Result<String> {
275 let renderer = Renderer::new(criterion_dir)
276 .benchmarks(allowlist)
277 .baseline(&options.baseline)
278 .title(&options.title)
279 .collapsible(options.collapsible);
280 renderer.render()
281}