1use std::{fmt::Write as _, fs, io, path::Path};
7
8use serde_json::Value;
9
10use super::{
11 BenchmarkAggregateRow, BenchmarkAverages, BenchmarkComparisonReport, BenchmarkEventKind,
12 BenchmarkRunMetadata, BenchmarkRunReport, BenchmarkScope, BenchmarkSpan, InvalidBenchmarkSpan,
13 MalformedBenchmarkMarker, RawBenchmarkEvent,
14};
15
16const ALL_SUITES_LABEL: &str = "ALL";
17
18impl BenchmarkScope {
19 fn label(&self) -> &str {
20 match self {
21 Self::Suite(suite) => suite,
22 Self::All => ALL_SUITES_LABEL,
23 }
24 }
25}
26
27pub fn read_benchmark_run_metadata(path: impl AsRef<Path>) -> io::Result<BenchmarkRunMetadata> {
28 let input = fs::read_to_string(path)?;
29 let value = serde_json::from_str::<Value>(&input).map_err(metadata_json_error)?;
30
31 if !value.is_object() {
32 return Err(io::Error::new(
33 io::ErrorKind::InvalidData,
34 "benchmark metadata JSON must be an object",
35 ));
36 }
37 serde_json::from_value(value).map_err(metadata_json_error)
38}
39
40pub fn write_benchmark_report_dir(
45 report: &BenchmarkRunReport,
46 path: impl AsRef<Path>,
47) -> io::Result<()> {
48 let path = path.as_ref();
49 let suite_aggregates = aggregates_csv(
50 report
51 .aggregates
52 .rows
53 .iter()
54 .filter(|row| matches!(row.scope, BenchmarkScope::Suite(_))),
55 )?;
56 let all_aggregates = aggregates_csv(
57 report
58 .aggregates
59 .rows
60 .iter()
61 .filter(|row| matches!(row.scope, BenchmarkScope::All)),
62 )?;
63 let summary = benchmark_summary_markdown(report)?;
64 fs::create_dir_all(path)?;
65
66 fs::write(
67 path.join("raw-events.csv"),
68 raw_events_csv(&report.parse.events),
69 )?;
70 fs::write(
71 path.join("malformed-markers.csv"),
72 malformed_markers_csv(&report.parse.malformed_markers),
73 )?;
74 fs::write(path.join("spans.csv"), spans_csv(&report.spans.spans))?;
75 fs::write(
76 path.join("unpaired-markers.csv"),
77 unpaired_markers_csv(&report.spans.unpaired_markers),
78 )?;
79 fs::write(
80 path.join("invalid-spans.csv"),
81 invalid_spans_csv(&report.spans.invalid_spans),
82 )?;
83 fs::write(path.join("suite-aggregates.csv"), suite_aggregates)?;
84 fs::write(path.join("all-aggregates.csv"), all_aggregates)?;
85 fs::write(
86 path.join("comparison.csv"),
87 comparison_csv(report.comparison.as_ref()),
88 )?;
89 fs::write(path.join("bench-summary.md"), summary)?;
90 fs::write(path.join("metadata.json"), metadata_json(&report.metadata))?;
91
92 Ok(())
93}
94
95fn raw_events_csv(events: &[RawBenchmarkEvent]) -> String {
96 let mut out = String::from(
97 "source_line,source,prefix,suite,label,span_label,kind,instructions,heap_bytes,memory_bytes,total_allocation\n",
98 );
99 for event in events {
100 let _ = writeln!(
101 out,
102 "{},{},{},{},{},{},{},{},{},{},{}",
103 event.source_line,
104 event.source.as_str(),
105 csv_cell(&event.prefix),
106 csv_cell(&event.suite),
107 csv_cell(&format!("{}:{}", event.span_label, kind_str(event.kind))),
108 csv_cell(&event.span_label),
109 kind_str(event.kind),
110 event.counters.instructions,
111 event.counters.heap_bytes,
112 event.counters.memory_bytes,
113 event.counters.total_allocation
114 );
115 }
116 out
117}
118
119fn malformed_markers_csv(markers: &[MalformedBenchmarkMarker]) -> String {
120 let mut out = String::from("source_line,source,reason,line\n");
121 for marker in markers {
122 let _ = writeln!(
123 out,
124 "{},{},{},{}",
125 marker.source_line,
126 marker.source.as_str(),
127 csv_cell(&marker.reason),
128 csv_cell(&marker.line)
129 );
130 }
131 out
132}
133
134fn spans_csv(spans: &[BenchmarkSpan]) -> String {
135 let mut out = String::from(
136 "suite,span_label,start_line,end_line,instructions_delta,heap_bytes_delta,memory_bytes_delta,total_allocation_delta\n",
137 );
138 for span in spans {
139 let _ = writeln!(
140 out,
141 "{},{},{},{},{},{},{},{}",
142 csv_cell(&span.suite),
143 csv_cell(&span.span_label),
144 span.start_line,
145 span.end_line,
146 span.delta.instructions,
147 span.delta.heap_bytes,
148 span.delta.memory_bytes,
149 span.delta.total_allocation
150 );
151 }
152 out
153}
154
155fn unpaired_markers_csv(markers: &[RawBenchmarkEvent]) -> String {
156 let mut out = String::from("source_line,source,kind,suite,span_label,label\n");
157 for marker in markers {
158 let kind = kind_str(marker.kind);
159 let _ = writeln!(
160 out,
161 "{},{},{},{},{},{}",
162 marker.source_line,
163 marker.source.as_str(),
164 kind,
165 csv_cell(&marker.suite),
166 csv_cell(&marker.span_label),
167 csv_cell(&format!("{}:{kind}", marker.span_label))
168 );
169 }
170 out
171}
172
173fn invalid_spans_csv(spans: &[InvalidBenchmarkSpan]) -> String {
174 let mut out = String::from("start_line,end_line,suite,span_label,reason\n");
175 for span in spans {
176 let _ = writeln!(
177 out,
178 "{},{},{},{},{}",
179 span.start.source_line,
180 span.end.source_line,
181 csv_cell(&span.start.suite),
182 csv_cell(&span.start.span_label),
183 csv_cell(&span.reason)
184 );
185 }
186 out
187}
188
189fn aggregates_csv<'a>(rows: impl Iterator<Item = &'a BenchmarkAggregateRow>) -> io::Result<String> {
190 let mut out = String::from(
191 "suite,span_label,runs,instructions_total,instructions_mean,heap_bytes_total,heap_bytes_mean,memory_bytes_total,memory_bytes_mean,total_allocation_total,total_allocation_mean\n",
192 );
193 for row in rows {
194 let average = row.average(1).map_err(io::Error::other)?;
195 let _ = writeln!(
196 out,
197 "{},{},{},{},{},{},{},{},{},{},{}",
198 csv_cell(row.scope.label()),
199 csv_cell(&row.span_label),
200 row.runs,
201 row.total.instructions,
202 average.instructions,
203 row.total.heap_bytes,
204 average.heap_bytes,
205 row.total.memory_bytes,
206 average.memory_bytes,
207 row.total.total_allocation,
208 average.total_allocation
209 );
210 }
211 Ok(out)
212}
213
214fn comparison_csv(comparison: Option<&BenchmarkComparisonReport>) -> String {
215 let mut out = String::from(
216 "scope,suite,span_label,current_runs,previous_runs,current_mean_scale,previous_mean_scale,current_instructions_mean,previous_instructions_mean,current_heap_bytes_mean,previous_heap_bytes_mean,current_memory_bytes_mean,previous_memory_bytes_mean,current_total_allocation_mean,previous_total_allocation_mean\n",
217 );
218 if let Some(comparison) = comparison {
219 for row in &comparison.rows {
220 let current = row.current.map(|measurement| measurement.average);
221 let previous = row.previous.map(|measurement| measurement.average);
222 let value = |mean: Option<BenchmarkAverages>, field: fn(BenchmarkAverages) -> u128| {
223 mean.map_or_else(String::new, |mean| field(mean).to_string())
224 };
225 let _ = writeln!(
226 out,
227 "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
228 if matches!(row.scope, BenchmarkScope::All) {
229 "all"
230 } else {
231 "suite"
232 },
233 csv_cell(row.scope.label()),
234 csv_cell(&row.span_label),
235 optional_u64_cell(row.current.map(|measurement| measurement.runs)),
236 optional_u64_cell(row.previous.map(|measurement| measurement.runs)),
237 current.map_or_else(String::new, |mean| mean.scale.to_string()),
238 previous.map_or_else(String::new, |mean| mean.scale.to_string()),
239 value(current, |m| m.instructions),
240 value(previous, |m| m.instructions),
241 value(current, |m| m.heap_bytes),
242 value(previous, |m| m.heap_bytes),
243 value(current, |m| m.memory_bytes),
244 value(previous, |m| m.memory_bytes),
245 value(current, |m| m.total_allocation),
246 value(previous, |m| m.total_allocation)
247 );
248 }
249 }
250 out
251}
252
253fn benchmark_summary_markdown(report: &BenchmarkRunReport) -> io::Result<String> {
254 let mut out = String::from(
255 "# Benchmark Summary\n\nMeans are floor-rounded in original counter units. Comparison CSV retains current and previous scaled means.\n\n| Benchmark | Runs | Instructions Mean | Heap Bytes Mean | Memory Bytes Mean | Allocation Mean |\n| --- | ---: | ---: | ---: | ---: | ---: |\n",
256 );
257 for row in report
258 .aggregates
259 .rows
260 .iter()
261 .filter(|row| matches!(row.scope, BenchmarkScope::Suite(_)))
262 {
263 let average = row.average(1).map_err(io::Error::other)?;
264 let _ = writeln!(
265 out,
266 "| {} | {} | {} | {} | {} | {} |",
267 markdown_cell(&row.span_label),
268 row.runs,
269 average.instructions,
270 average.heap_bytes,
271 average.memory_bytes,
272 average.total_allocation
273 );
274 }
275 Ok(out)
276}
277
278fn metadata_json(metadata: &BenchmarkRunMetadata) -> String {
279 let mut output = serde_json::to_string_pretty(metadata).expect("metadata JSON must serialize");
280 output.push('\n');
281 output
282}
283
284fn metadata_json_error(err: serde_json::Error) -> io::Error {
285 io::Error::new(
286 io::ErrorKind::InvalidData,
287 format!("invalid benchmark metadata JSON: {err}"),
288 )
289}
290
291const fn kind_str(kind: BenchmarkEventKind) -> &'static str {
292 match kind {
293 BenchmarkEventKind::Start => "start",
294 BenchmarkEventKind::End => "end",
295 }
296}
297
298fn csv_cell(value: &str) -> String {
299 if value.contains([',', '"', '\n', '\r']) {
300 format!("\"{}\"", value.replace('"', "\"\""))
301 } else {
302 value.to_string()
303 }
304}
305
306fn optional_u64_cell(value: Option<u64>) -> String {
307 value.map_or_else(String::new, |value| value.to_string())
308}
309
310fn markdown_cell(value: &str) -> String {
311 value.replace('|', "\\|")
312}