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