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