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