criterion_markdown/
lib.rs1use std::path::{Path, PathBuf};
32
33use anyhow::Result;
34
35mod discovery;
36mod markdown;
37mod model;
38
39#[derive(Debug, Clone, Copy)]
44pub struct Thresholds {
45 improvement_ratio: f64,
46 strong_improvement_ratio: f64,
47 regression_ratio: f64,
48}
49
50impl Thresholds {
51 pub fn improvement_ratio(mut self, ratio: f64) -> Self {
53 self.improvement_ratio = ratio;
54 self
55 }
56
57 pub fn strong_improvement_ratio(mut self, ratio: f64) -> Self {
59 self.strong_improvement_ratio = ratio;
60 self
61 }
62
63 pub fn regression_ratio(mut self, ratio: f64) -> Self {
65 self.regression_ratio = ratio;
66 self
67 }
68
69 fn validate(self) -> Result<()> {
70 if !self.improvement_ratio.is_finite() || self.improvement_ratio < 1.0 {
71 anyhow::bail!("improvement ratio must be finite and at least 1.0");
72 }
73 if !self.strong_improvement_ratio.is_finite()
74 || self.strong_improvement_ratio < self.improvement_ratio
75 {
76 anyhow::bail!(
77 "strong improvement ratio must be finite and at least the improvement ratio"
78 );
79 }
80 if !self.regression_ratio.is_finite()
81 || self.regression_ratio <= 0.0
82 || self.regression_ratio > 1.0
83 {
84 anyhow::bail!("regression ratio must be finite, positive, and at most 1.0");
85 }
86 Ok(())
87 }
88}
89
90impl Default for Thresholds {
91 fn default() -> Self {
92 Self {
93 improvement_ratio: 1.1,
94 strong_improvement_ratio: 1.8,
95 regression_ratio: 0.9,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct Emojis {
103 pub(crate) regression: String,
104 pub(crate) stable: String,
105 pub(crate) improvement: String,
106 pub(crate) strong_improvement: String,
107}
108
109impl Emojis {
110 pub fn regression(mut self, emoji: impl AsRef<str>) -> Self {
112 self.regression = emoji.as_ref().to_string();
113 self
114 }
115
116 pub fn stable(mut self, emoji: impl AsRef<str>) -> Self {
118 self.stable = emoji.as_ref().to_string();
119 self
120 }
121
122 pub fn improvement(mut self, emoji: impl AsRef<str>) -> Self {
124 self.improvement = emoji.as_ref().to_string();
125 self
126 }
127
128 pub fn strong_improvement(mut self, emoji: impl AsRef<str>) -> Self {
130 self.strong_improvement = emoji.as_ref().to_string();
131 self
132 }
133}
134
135impl Default for Emojis {
136 fn default() -> Self {
137 Self {
138 regression: "❌".to_string(),
139 stable: "➖".to_string(),
140 improvement: "↗️".to_string(),
141 strong_improvement: "🚀".to_string(),
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct Renderer {
149 criterion_dir: PathBuf,
150 baseline_root: PathBuf,
151 candidate: String,
152 baseline: Option<String>,
153 included_entries: Vec<String>,
154 title: String,
155 collapsible: bool,
156 thresholds: Thresholds,
157 emojis: Emojis,
158 summary_limit: usize,
159}
160
161impl Renderer {
162 pub fn new(criterion_dir: impl AsRef<Path>) -> Self {
164 let criterion_dir = criterion_dir.as_ref().to_path_buf();
165 Self {
166 baseline_root: criterion_dir.clone(),
167 criterion_dir,
168 candidate: "new".to_string(),
169 baseline: None,
170 included_entries: Vec::new(),
171 title: "Benchmarks".to_string(),
172 collapsible: false,
173 thresholds: Thresholds::default(),
174 emojis: Emojis::default(),
175 summary_limit: 3,
176 }
177 }
178
179 pub fn candidate(mut self, candidate: impl AsRef<str>) -> Self {
181 self.candidate = candidate.as_ref().to_string();
182 self
183 }
184
185 pub fn baseline(mut self, baseline: impl AsRef<str>) -> Self {
191 self.baseline = Some(baseline.as_ref().to_string());
192 self
193 }
194
195 pub fn baseline_root(mut self, baseline_root: impl AsRef<Path>) -> Self {
199 self.baseline_root = baseline_root.as_ref().to_path_buf();
200 self
201 }
202
203 pub fn benchmark(mut self, entry: impl AsRef<str>) -> Self {
207 self.included_entries.push(entry.as_ref().to_string());
208 self
209 }
210
211 pub fn benchmarks(mut self, entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
216 self.included_entries
217 .extend(entries.into_iter().map(|entry| entry.as_ref().to_string()));
218 self
219 }
220
221 pub fn title(mut self, title: impl AsRef<str>) -> Self {
223 self.title = title.as_ref().to_string();
224 self
225 }
226
227 pub fn collapsible(mut self, collapsible: bool) -> Self {
229 self.collapsible = collapsible;
230 self
231 }
232
233 pub fn thresholds(mut self, thresholds: Thresholds) -> Self {
235 self.thresholds = thresholds;
236 self
237 }
238
239 pub fn emojis(mut self, emojis: Emojis) -> Self {
241 self.emojis = emojis;
242 self
243 }
244
245 pub fn summary_limit(mut self, limit: usize) -> Self {
248 self.summary_limit = limit;
249 self
250 }
251
252 pub fn render(&self) -> Result<String> {
254 self.thresholds.validate()?;
255 let mut entries = discovery::discover_benchmarks(
256 &self.criterion_dir,
257 &self.candidate,
258 &self.baseline_root,
259 self.baseline.as_deref(),
260 )?;
261 if !self.included_entries.is_empty() {
262 entries.retain(|entry| self.included_entries.contains(&entry.full_id));
263 }
264 if entries.is_empty() {
265 anyhow::bail!(
266 "No benchmark results found in {}",
267 self.criterion_dir.display()
268 );
269 }
270 let summary =
271 markdown::compute_summary(&entries, self.thresholds, &self.emojis, self.summary_limit);
272 let body = markdown::format_table(
273 &entries,
274 &self.title,
275 self.collapsible,
276 self.thresholds,
277 &self.emojis,
278 &summary,
279 );
280 if !self.collapsible {
281 return Ok(body);
282 }
283 let headline = summary
284 .headline(&self.emojis)
285 .map(|headline| format!(" ({headline})"))
286 .unwrap_or_default();
287 Ok(format!(
288 "<details>\n<summary>{}{headline}</summary>\n\n{body}\n</details>\n",
289 self.title
290 ))
291 }
292}