1#![deny(missing_docs)]
30
31use std::collections::HashMap;
32use std::fs::OpenOptions;
33use std::io::Write;
34use std::ops::{Deref, DerefMut};
35use std::path::{Path, PathBuf};
36use std::{env, process};
37
38use criterion::Criterion;
39use tempfile::TempDir;
40use walkdir::WalkDir;
41
42pub use criterion;
43
44pub struct FuchsiaCriterion {
46 criterion: Criterion,
47 output: Option<(TempDir, PathBuf)>,
48}
49
50impl FuchsiaCriterion {
51 pub fn new(criterion: Criterion) -> Self {
53 Self { criterion, output: None }
54 }
55
56 pub fn fuchsia_bench() -> Self {
64 let args: Vec<String> = env::args().collect();
65 let args: Vec<&str> = args.iter().map(|s| &**s).collect();
66 Self::fuchsia_bench_with_args(&args[..])
67 }
68
69 pub fn fuchsia_bench_with_args(args: &[&str]) -> Self {
75 fn help_and_exit(name: &str, wrong_args: Option<String>) -> ! {
76 println!(
77 "fuchsia-criterion benchmark\n\
78 \n\
79 USAGE: {} [FLAGS] JSON_OUTPUT\n\
80 \n\
81 FLAGS:\n\
82 --filter <string> Only runs benchmarks with names that contain the given string\n\
83 -h, --help Prints help information",
84 name,
85 );
86
87 if let Some(wrong_args) = wrong_args {
88 eprintln!("error: unrecognized args: {}", wrong_args);
89 process::exit(1)
90 } else {
91 process::exit(0)
92 }
93 }
94
95 match &args[..] {
96 [_, "--filter", filter, json_output] => {
97 let output_directory =
98 TempDir::new().expect("failed to access temporary directory");
99 let criterion = Criterion::default()
100 .nresamples(10_000)
101 .with_filter(*filter)
102 .output_directory(output_directory.path());
103 Self {
104 criterion,
105 output: Some((output_directory, Path::new(*json_output).to_path_buf())),
106 }
107 }
108 [_, arg] if !arg.starts_with('-') => {
109 let output_directory =
110 TempDir::new().expect("failed to access temporary directory");
111 let criterion = Criterion::default()
112 .nresamples(10_000)
113 .output_directory(output_directory.path());
114
115 Self {
116 criterion,
117 output: Some((output_directory, Path::new(&args[1]).to_path_buf())),
118 }
119 }
120 [name, "-h"] | [name, "--help"] => help_and_exit(name, None),
121 _ => help_and_exit(&args[0], Some(args[1..].join(" "))),
122 }
123 }
124
125 fn convert_csvs(output_directory: &Path, output_json: &Path) {
126 let csv_entries = WalkDir::new(output_directory).into_iter().filter_map(|res| {
127 res.ok().filter(|entry| {
128 let path = entry.path();
129 path.is_file() && path.extension().map(|ext| ext == "csv").unwrap_or(false)
130 })
131 });
132
133 let mut results: HashMap<_, Vec<f64>> = HashMap::new();
134
135 for csv_entry in csv_entries {
136 let mut reader = csv::Reader::from_path(csv_entry.path())
137 .expect("found non-CSV file in fuchsia-criterion specific temporary directory");
138
139 for record in reader.records() {
140 let record = record.expect("CSV record is not UTF-8");
141
142 let (label, test_suite, sample) = match record.len() {
143 5 => {
144 let label = record[1].to_string();
145 let test_suite = record[0].to_string();
146 let sample = match (record[3].parse::<f64>(), record[4].parse::<f64>()) {
147 (Ok(time), Ok(iter_count)) => time / iter_count,
148 _ => panic!("non floating point values in Criterion-generated CSV"),
149 };
150 (label, test_suite, sample)
151 }
152 8 => {
155 let group = record[0].to_string();
156 let function = &record[1];
157 let value = &record[2];
158 let label = match (!function.is_empty(), !value.is_empty()) {
159 (true, true) => format!("{function}/{value}"),
160 (true, false) => function.to_string(),
161 (false, true) => value.to_string(),
162 (false, false) => "".to_string(),
163 };
164 let sample = match (record[5].parse::<f64>(), record[7].parse::<f64>()) {
165 (Ok(time), Ok(iter_count)) => time / iter_count,
166 _ => panic!("non floating point values in Criterion-generated CSV"),
167 };
168 (label, group, sample)
169 }
170 _ => panic!("wrong number of records in Criterion-generated CSV"),
171 };
172
173 results
174 .entry((label, test_suite))
175 .and_modify(|samples| samples.push(sample))
176 .or_insert_with(|| vec![sample]);
177 }
178 }
179
180 let entries: Vec<_> = results
181 .into_iter()
182 .map(|((label, test_suite), samples)| {
183 format!(
184 r#"{{
185 "label": {:?},
186 "test_suite": {:?},
187 "unit": "nanoseconds",
188 "values": {:?}
189 }}"#,
190 label, test_suite, samples,
191 )
192 })
193 .collect();
194
195 let mut f = OpenOptions::new()
196 .truncate(true)
197 .write(true)
198 .create(true)
199 .open(output_json)
200 .expect("failed to open output JSON");
201 write!(f, "[\n {}\n]\n", entries.join(",\n"),).expect("failed to write to output JSON");
202 }
203}
204
205impl Default for FuchsiaCriterion {
206 fn default() -> Self {
207 if cfg!(feature = "local_bench") {
208 let criterion = Criterion::default()
209 .nresamples(10_000)
210 .output_directory(Path::new("/tmp/criterion"))
211 .configure_from_args();
212
213 FuchsiaCriterion::new(criterion)
214 } else {
215 FuchsiaCriterion::fuchsia_bench()
216 }
217 }
218}
219
220impl Deref for FuchsiaCriterion {
221 type Target = Criterion;
222
223 fn deref(&self) -> &Self::Target {
224 &self.criterion
225 }
226}
227
228impl DerefMut for FuchsiaCriterion {
229 fn deref_mut(&mut self) -> &mut Self::Target {
230 &mut self.criterion
231 }
232}
233
234impl Drop for FuchsiaCriterion {
235 fn drop(&mut self) {
240 self.criterion.final_summary();
241
242 if let Some((output_directory, output_json)) = &self.output {
243 Self::convert_csvs(output_directory.path(), output_json);
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 use std::fs::{self, File};
253 use std::io;
254
255 #[test]
256 fn criterion_results_conversion() -> io::Result<()> {
257 let temp = TempDir::new()?;
258 let output_directory = temp.path().join("some/where/deep");
259
260 fs::create_dir_all(output_directory.clone())?;
261
262 let mut csv = File::create(output_directory.join("criterion.csv"))?;
263 csv.write_all(
264 b"\
265 group,function,value,sample_time_nanos,iteration_count\n\
266 fib,parallel,,1000000,100000\n\
267 fib,parallel,,2000000,200000\n\
268 fib,parallel,,4000000,300000\n\
269 fib,parallel,,6000000,400000\n\
270 ",
271 )?;
272
273 let json = output_directory.join("fuchsia.json");
274 FuchsiaCriterion::convert_csvs(&output_directory, &json);
275
276 assert_eq!(
277 fs::read_to_string(json)?,
278 r#"[
279 {
280 "label": "parallel",
281 "test_suite": "fib",
282 "unit": "nanoseconds",
283 "values": [10.0, 10.0, 13.333333333333334, 15.0]
284 }
285]
286"#
287 );
288
289 Ok(())
290 }
291
292 #[test]
293 fn criterion_0_8_results_conversion() -> io::Result<()> {
294 let temp = TempDir::new()?;
295 let output_directory = temp.path().join("some/where/deep");
296
297 fs::create_dir_all(output_directory.clone())?;
298
299 let mut csv = File::create(output_directory.join("raw.csv"))?;
300 csv.write_all(
301 b"\
302group,function,value,throughput_num,throughput_type,sample_measured_value,unit,iteration_count\n\
303fib,parallel,,,,1000000.0,ns,100000\n\
304fib,parallel,,,,2000000.0,ns,200000\n\
305fib,parallel,,,,4000000.0,ns,300000\n\
306fib,parallel,,,,6000000.0,ns,400000\n\
307",
308 )?;
309
310 let json = output_directory.join("fuchsia.json");
311 FuchsiaCriterion::convert_csvs(&output_directory, &json);
312
313 assert_eq!(
314 fs::read_to_string(json)?,
315 r#"[
316 {
317 "label": "parallel",
318 "test_suite": "fib",
319 "unit": "nanoseconds",
320 "values": [10.0, 10.0, 13.333333333333334, 15.0]
321 }
322]
323"#
324 );
325
326 Ok(())
327 }
328}