xref: /cloud-hypervisor/performance-metrics/src/main.rs (revision 88a9f799449c04180c6b9a21d3b9c0c4b57e2bd6)
1 // Copyright © 2022 Intel Corporation
2 //
3 // SPDX-License-Identifier: Apache-2.0
4 //
5 
6 // Custom harness to run performance tests
7 extern crate test_infra;
8 
9 mod performance_tests;
10 
11 use std::{
12     env, fmt,
13     process::Command,
14     sync::{mpsc::channel, Arc},
15     thread,
16     time::Duration,
17 };
18 
19 use clap::{Arg, ArgAction, Command as ClapCommand};
20 use performance_tests::*;
21 use serde::{Deserialize, Serialize};
22 use test_infra::FioOps;
23 use thiserror::Error;
24 
25 #[derive(Error, Debug)]
26 enum Error {
27     #[error("Error: test timed-out")]
28     TestTimeout,
29     #[error("Error: test failed")]
30     TestFailed,
31 }
32 
33 #[derive(Deserialize, Serialize)]
34 pub struct PerformanceTestResult {
35     name: String,
36     mean: f64,
37     std_dev: f64,
38     max: f64,
39     min: f64,
40 }
41 
42 #[derive(Deserialize, Serialize)]
43 pub struct MetricsReport {
44     pub git_human_readable: String,
45     pub git_revision: String,
46     pub git_commit_date: String,
47     pub date: String,
48     pub results: Vec<PerformanceTestResult>,
49 }
50 
51 impl Default for MetricsReport {
52     fn default() -> Self {
53         let mut git_human_readable = "".to_string();
54         if let Ok(git_out) = Command::new("git").args(["describe", "--dirty"]).output() {
55             if git_out.status.success() {
56                 git_human_readable = String::from_utf8(git_out.stdout)
57                     .unwrap()
58                     .trim()
59                     .to_string();
60             } else {
61                 eprintln!(
62                     "Error generating human readable git reference: {}",
63                     String::from_utf8(git_out.stderr).unwrap()
64                 );
65             }
66         }
67 
68         let mut git_revision = "".to_string();
69         if let Ok(git_out) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
70             if git_out.status.success() {
71                 git_revision = String::from_utf8(git_out.stdout)
72                     .unwrap()
73                     .trim()
74                     .to_string();
75             } else {
76                 eprintln!(
77                     "Error generating git reference: {}",
78                     String::from_utf8(git_out.stderr).unwrap()
79                 );
80             }
81         }
82 
83         let mut git_commit_date = "".to_string();
84         if let Ok(git_out) = Command::new("git")
85             .args(["show", "-s", "--format=%cd"])
86             .output()
87         {
88             if git_out.status.success() {
89                 git_commit_date = String::from_utf8(git_out.stdout)
90                     .unwrap()
91                     .trim()
92                     .to_string();
93             } else {
94                 eprintln!(
95                     "Error generating git commit date: {}",
96                     String::from_utf8(git_out.stderr).unwrap()
97                 );
98             }
99         }
100 
101         MetricsReport {
102             git_human_readable,
103             git_revision,
104             git_commit_date,
105             date: date(),
106             results: Vec::new(),
107         }
108     }
109 }
110 
111 #[derive(Default)]
112 pub struct PerformanceTestOverrides {
113     test_iterations: Option<u32>,
114     test_timeout: Option<u32>,
115 }
116 
117 impl fmt::Display for PerformanceTestOverrides {
118     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119         if let Some(test_iterations) = self.test_iterations {
120             write!(f, "test_iterations = {test_iterations}, ")?;
121         }
122         if let Some(test_timeout) = self.test_timeout {
123             write!(f, "test_timeout = {test_timeout}")?;
124         }
125 
126         Ok(())
127     }
128 }
129 
130 #[derive(Clone)]
131 pub struct PerformanceTestControl {
132     test_timeout: u32,
133     test_iterations: u32,
134     num_queues: Option<u32>,
135     queue_size: Option<u32>,
136     net_control: Option<(bool, bool)>, // First bool is for RX(true)/TX(false), second bool is for bandwidth or PPS
137     fio_control: Option<(FioOps, bool)>, // Second parameter controls whether we want bandwidth or IOPS
138     num_boot_vcpus: Option<u8>,
139 }
140 
141 impl fmt::Display for PerformanceTestControl {
142     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143         let mut output = format!(
144             "test_timeout = {}s, test_iterations = {}",
145             self.test_timeout, self.test_iterations
146         );
147         if let Some(o) = self.num_queues {
148             output = format!("{output}, num_queues = {o}");
149         }
150         if let Some(o) = self.queue_size {
151             output = format!("{output}, queue_size = {o}");
152         }
153         if let Some(o) = self.net_control {
154             let (rx, bw) = o;
155             output = format!("{output}, rx = {rx}, bandwidth = {bw}");
156         }
157         if let Some(o) = &self.fio_control {
158             let (ops, bw) = o;
159             output = format!("{output}, fio_ops = {ops}, bandwidth = {bw}");
160         }
161 
162         write!(f, "{output}")
163     }
164 }
165 
166 impl PerformanceTestControl {
167     const fn default() -> Self {
168         Self {
169             test_timeout: 10,
170             test_iterations: 5,
171             num_queues: None,
172             queue_size: None,
173             net_control: None,
174             fio_control: None,
175             num_boot_vcpus: Some(1),
176         }
177     }
178 }
179 
180 /// A performance test should finish within the a certain time-out and
181 /// return a performance metrics number (including the average number and
182 /// standard deviation)
183 struct PerformanceTest {
184     pub name: &'static str,
185     pub func_ptr: fn(&PerformanceTestControl) -> f64,
186     pub control: PerformanceTestControl,
187     unit_adjuster: fn(f64) -> f64,
188 }
189 
190 impl PerformanceTest {
191     pub fn run(&self, overrides: &PerformanceTestOverrides) -> PerformanceTestResult {
192         let mut metrics = Vec::new();
193         for _ in 0..overrides
194             .test_iterations
195             .unwrap_or(self.control.test_iterations)
196         {
197             // update the timeout in control if passed explicitly and run testcase with it
198             if let Some(test_timeout) = overrides.test_timeout {
199                 let mut control: PerformanceTestControl = self.control.clone();
200                 control.test_timeout = test_timeout;
201                 metrics.push((self.func_ptr)(&control));
202             } else {
203                 metrics.push((self.func_ptr)(&self.control));
204             }
205         }
206 
207         let mean = (self.unit_adjuster)(mean(&metrics).unwrap());
208         let std_dev = (self.unit_adjuster)(std_deviation(&metrics).unwrap());
209         let max = (self.unit_adjuster)(metrics.clone().into_iter().reduce(f64::max).unwrap());
210         let min = (self.unit_adjuster)(metrics.clone().into_iter().reduce(f64::min).unwrap());
211 
212         PerformanceTestResult {
213             name: self.name.to_string(),
214             mean,
215             std_dev,
216             max,
217             min,
218         }
219     }
220 
221     // Calculate the timeout for each test
222     // Note: To cover the setup/cleanup time, 20s is added for each iteration of the test
223     pub fn calc_timeout(&self, test_iterations: &Option<u32>, test_timeout: &Option<u32>) -> u64 {
224         ((test_timeout.unwrap_or(self.control.test_timeout) + 20)
225             * test_iterations.unwrap_or(self.control.test_iterations)) as u64
226     }
227 }
228 
229 fn mean(data: &[f64]) -> Option<f64> {
230     let count = data.len();
231 
232     if count > 0 {
233         Some(data.iter().sum::<f64>() / count as f64)
234     } else {
235         None
236     }
237 }
238 
239 fn std_deviation(data: &[f64]) -> Option<f64> {
240     let count = data.len();
241 
242     if count > 0 {
243         let mean = mean(data).unwrap();
244         let variance = data
245             .iter()
246             .map(|value| {
247                 let diff = mean - *value;
248                 diff * diff
249             })
250             .sum::<f64>()
251             / count as f64;
252 
253         Some(variance.sqrt())
254     } else {
255         None
256     }
257 }
258 
259 mod adjuster {
260     pub fn identity(v: f64) -> f64 {
261         v
262     }
263 
264     pub fn s_to_ms(v: f64) -> f64 {
265         v * 1000.0
266     }
267 
268     pub fn bps_to_gbps(v: f64) -> f64 {
269         v / (1_000_000_000_f64)
270     }
271 
272     #[allow(non_snake_case)]
273     pub fn Bps_to_MiBps(v: f64) -> f64 {
274         v / (1 << 20) as f64
275     }
276 }
277 
278 const TEST_LIST: [PerformanceTest; 30] = [
279     PerformanceTest {
280         name: "boot_time_ms",
281         func_ptr: performance_boot_time,
282         control: PerformanceTestControl {
283             test_timeout: 2,
284             test_iterations: 10,
285             ..PerformanceTestControl::default()
286         },
287         unit_adjuster: adjuster::s_to_ms,
288     },
289     PerformanceTest {
290         name: "boot_time_pmem_ms",
291         func_ptr: performance_boot_time_pmem,
292         control: PerformanceTestControl {
293             test_timeout: 2,
294             test_iterations: 10,
295             ..PerformanceTestControl::default()
296         },
297         unit_adjuster: adjuster::s_to_ms,
298     },
299     PerformanceTest {
300         name: "boot_time_16_vcpus_ms",
301         func_ptr: performance_boot_time,
302         control: PerformanceTestControl {
303             test_timeout: 2,
304             test_iterations: 10,
305             num_boot_vcpus: Some(16),
306             ..PerformanceTestControl::default()
307         },
308         unit_adjuster: adjuster::s_to_ms,
309     },
310     PerformanceTest {
311         name: "restore_latency_time_ms",
312         func_ptr: performance_restore_latency,
313         control: PerformanceTestControl {
314             test_timeout: 2,
315             test_iterations: 10,
316             ..PerformanceTestControl::default()
317         },
318         unit_adjuster: adjuster::identity,
319     },
320     PerformanceTest {
321         name: "boot_time_16_vcpus_pmem_ms",
322         func_ptr: performance_boot_time_pmem,
323         control: PerformanceTestControl {
324             test_timeout: 2,
325             test_iterations: 10,
326             num_boot_vcpus: Some(16),
327             ..PerformanceTestControl::default()
328         },
329         unit_adjuster: adjuster::s_to_ms,
330     },
331     PerformanceTest {
332         name: "virtio_net_latency_us",
333         func_ptr: performance_net_latency,
334         control: PerformanceTestControl {
335             num_queues: Some(2),
336             queue_size: Some(256),
337             ..PerformanceTestControl::default()
338         },
339         unit_adjuster: adjuster::identity,
340     },
341     PerformanceTest {
342         name: "virtio_net_throughput_single_queue_rx_gbps",
343         func_ptr: performance_net_throughput,
344         control: PerformanceTestControl {
345             num_queues: Some(2),
346             queue_size: Some(256),
347             net_control: Some((true, true)),
348             ..PerformanceTestControl::default()
349         },
350         unit_adjuster: adjuster::bps_to_gbps,
351     },
352     PerformanceTest {
353         name: "virtio_net_throughput_single_queue_tx_gbps",
354         func_ptr: performance_net_throughput,
355         control: PerformanceTestControl {
356             num_queues: Some(2),
357             queue_size: Some(256),
358             net_control: Some((false, true)),
359             ..PerformanceTestControl::default()
360         },
361         unit_adjuster: adjuster::bps_to_gbps,
362     },
363     PerformanceTest {
364         name: "virtio_net_throughput_multi_queue_rx_gbps",
365         func_ptr: performance_net_throughput,
366         control: PerformanceTestControl {
367             num_queues: Some(4),
368             queue_size: Some(256),
369             net_control: Some((true, true)),
370             ..PerformanceTestControl::default()
371         },
372         unit_adjuster: adjuster::bps_to_gbps,
373     },
374     PerformanceTest {
375         name: "virtio_net_throughput_multi_queue_tx_gbps",
376         func_ptr: performance_net_throughput,
377         control: PerformanceTestControl {
378             num_queues: Some(4),
379             queue_size: Some(256),
380             net_control: Some((false, true)),
381             ..PerformanceTestControl::default()
382         },
383         unit_adjuster: adjuster::bps_to_gbps,
384     },
385     PerformanceTest {
386         name: "virtio_net_throughput_single_queue_rx_pps",
387         func_ptr: performance_net_throughput,
388         control: PerformanceTestControl {
389             num_queues: Some(2),
390             queue_size: Some(256),
391             net_control: Some((true, false)),
392             ..PerformanceTestControl::default()
393         },
394         unit_adjuster: adjuster::identity,
395     },
396     PerformanceTest {
397         name: "virtio_net_throughput_single_queue_tx_pps",
398         func_ptr: performance_net_throughput,
399         control: PerformanceTestControl {
400             num_queues: Some(2),
401             queue_size: Some(256),
402             net_control: Some((false, false)),
403             ..PerformanceTestControl::default()
404         },
405         unit_adjuster: adjuster::identity,
406     },
407     PerformanceTest {
408         name: "virtio_net_throughput_multi_queue_rx_pps",
409         func_ptr: performance_net_throughput,
410         control: PerformanceTestControl {
411             num_queues: Some(4),
412             queue_size: Some(256),
413             net_control: Some((true, false)),
414             ..PerformanceTestControl::default()
415         },
416         unit_adjuster: adjuster::identity,
417     },
418     PerformanceTest {
419         name: "virtio_net_throughput_multi_queue_tx_pps",
420         func_ptr: performance_net_throughput,
421         control: PerformanceTestControl {
422             num_queues: Some(4),
423             queue_size: Some(256),
424             net_control: Some((false, false)),
425             ..PerformanceTestControl::default()
426         },
427         unit_adjuster: adjuster::identity,
428     },
429     PerformanceTest {
430         name: "block_read_MiBps",
431         func_ptr: performance_block_io,
432         control: PerformanceTestControl {
433             num_queues: Some(1),
434             queue_size: Some(128),
435             fio_control: Some((FioOps::Read, true)),
436             ..PerformanceTestControl::default()
437         },
438         unit_adjuster: adjuster::Bps_to_MiBps,
439     },
440     PerformanceTest {
441         name: "block_write_MiBps",
442         func_ptr: performance_block_io,
443         control: PerformanceTestControl {
444             num_queues: Some(1),
445             queue_size: Some(128),
446             fio_control: Some((FioOps::Write, true)),
447             ..PerformanceTestControl::default()
448         },
449         unit_adjuster: adjuster::Bps_to_MiBps,
450     },
451     PerformanceTest {
452         name: "block_random_read_MiBps",
453         func_ptr: performance_block_io,
454         control: PerformanceTestControl {
455             num_queues: Some(1),
456             queue_size: Some(128),
457             fio_control: Some((FioOps::RandomRead, true)),
458             ..PerformanceTestControl::default()
459         },
460         unit_adjuster: adjuster::Bps_to_MiBps,
461     },
462     PerformanceTest {
463         name: "block_random_write_MiBps",
464         func_ptr: performance_block_io,
465         control: PerformanceTestControl {
466             num_queues: Some(1),
467             queue_size: Some(128),
468             fio_control: Some((FioOps::RandomWrite, true)),
469             ..PerformanceTestControl::default()
470         },
471         unit_adjuster: adjuster::Bps_to_MiBps,
472     },
473     PerformanceTest {
474         name: "block_multi_queue_read_MiBps",
475         func_ptr: performance_block_io,
476         control: PerformanceTestControl {
477             num_queues: Some(2),
478             queue_size: Some(128),
479             fio_control: Some((FioOps::Read, true)),
480             ..PerformanceTestControl::default()
481         },
482         unit_adjuster: adjuster::Bps_to_MiBps,
483     },
484     PerformanceTest {
485         name: "block_multi_queue_write_MiBps",
486         func_ptr: performance_block_io,
487         control: PerformanceTestControl {
488             num_queues: Some(2),
489             queue_size: Some(128),
490             fio_control: Some((FioOps::Write, true)),
491             ..PerformanceTestControl::default()
492         },
493         unit_adjuster: adjuster::Bps_to_MiBps,
494     },
495     PerformanceTest {
496         name: "block_multi_queue_random_read_MiBps",
497         func_ptr: performance_block_io,
498         control: PerformanceTestControl {
499             num_queues: Some(2),
500             queue_size: Some(128),
501             fio_control: Some((FioOps::RandomRead, true)),
502             ..PerformanceTestControl::default()
503         },
504         unit_adjuster: adjuster::Bps_to_MiBps,
505     },
506     PerformanceTest {
507         name: "block_multi_queue_random_write_MiBps",
508         func_ptr: performance_block_io,
509         control: PerformanceTestControl {
510             num_queues: Some(2),
511             queue_size: Some(128),
512             fio_control: Some((FioOps::RandomWrite, true)),
513             ..PerformanceTestControl::default()
514         },
515         unit_adjuster: adjuster::Bps_to_MiBps,
516     },
517     PerformanceTest {
518         name: "block_read_IOPS",
519         func_ptr: performance_block_io,
520         control: PerformanceTestControl {
521             num_queues: Some(1),
522             queue_size: Some(128),
523             fio_control: Some((FioOps::Read, false)),
524             ..PerformanceTestControl::default()
525         },
526         unit_adjuster: adjuster::identity,
527     },
528     PerformanceTest {
529         name: "block_write_IOPS",
530         func_ptr: performance_block_io,
531         control: PerformanceTestControl {
532             num_queues: Some(1),
533             queue_size: Some(128),
534             fio_control: Some((FioOps::Write, false)),
535             ..PerformanceTestControl::default()
536         },
537         unit_adjuster: adjuster::identity,
538     },
539     PerformanceTest {
540         name: "block_random_read_IOPS",
541         func_ptr: performance_block_io,
542         control: PerformanceTestControl {
543             num_queues: Some(1),
544             queue_size: Some(128),
545             fio_control: Some((FioOps::RandomRead, false)),
546             ..PerformanceTestControl::default()
547         },
548         unit_adjuster: adjuster::identity,
549     },
550     PerformanceTest {
551         name: "block_random_write_IOPS",
552         func_ptr: performance_block_io,
553         control: PerformanceTestControl {
554             num_queues: Some(1),
555             queue_size: Some(128),
556             fio_control: Some((FioOps::RandomWrite, false)),
557             ..PerformanceTestControl::default()
558         },
559         unit_adjuster: adjuster::identity,
560     },
561     PerformanceTest {
562         name: "block_multi_queue_read_IOPS",
563         func_ptr: performance_block_io,
564         control: PerformanceTestControl {
565             num_queues: Some(2),
566             queue_size: Some(128),
567             fio_control: Some((FioOps::Read, false)),
568             ..PerformanceTestControl::default()
569         },
570         unit_adjuster: adjuster::identity,
571     },
572     PerformanceTest {
573         name: "block_multi_queue_write_IOPS",
574         func_ptr: performance_block_io,
575         control: PerformanceTestControl {
576             num_queues: Some(2),
577             queue_size: Some(128),
578             fio_control: Some((FioOps::Write, false)),
579             ..PerformanceTestControl::default()
580         },
581         unit_adjuster: adjuster::identity,
582     },
583     PerformanceTest {
584         name: "block_multi_queue_random_read_IOPS",
585         func_ptr: performance_block_io,
586         control: PerformanceTestControl {
587             num_queues: Some(2),
588             queue_size: Some(128),
589             fio_control: Some((FioOps::RandomRead, false)),
590             ..PerformanceTestControl::default()
591         },
592         unit_adjuster: adjuster::identity,
593     },
594     PerformanceTest {
595         name: "block_multi_queue_random_write_IOPS",
596         func_ptr: performance_block_io,
597         control: PerformanceTestControl {
598             num_queues: Some(2),
599             queue_size: Some(128),
600             fio_control: Some((FioOps::RandomWrite, false)),
601             ..PerformanceTestControl::default()
602         },
603         unit_adjuster: adjuster::identity,
604     },
605 ];
606 
607 fn run_test_with_timeout(
608     test: &'static PerformanceTest,
609     overrides: &Arc<PerformanceTestOverrides>,
610 ) -> Result<PerformanceTestResult, Error> {
611     let (sender, receiver) = channel::<Result<PerformanceTestResult, Error>>();
612     let test_iterations = overrides.test_iterations;
613     let test_timeout = overrides.test_timeout;
614     let overrides = overrides.clone();
615     thread::spawn(move || {
616         println!(
617             "Test '{}' running .. (control: {}, overrides: {})",
618             test.name, test.control, overrides
619         );
620 
621         let output = match std::panic::catch_unwind(|| test.run(&overrides)) {
622             Ok(test_result) => {
623                 println!(
624                     "Test '{}' .. ok: mean = {}, std_dev = {}",
625                     test_result.name, test_result.mean, test_result.std_dev
626                 );
627                 Ok(test_result)
628             }
629             Err(_) => Err(Error::TestFailed),
630         };
631 
632         let _ = sender.send(output);
633     });
634 
635     // Todo: Need to cleanup/kill all hanging child processes
636     let test_timeout = test.calc_timeout(&test_iterations, &test_timeout);
637     receiver
638         .recv_timeout(Duration::from_secs(test_timeout))
639         .map_err(|_| {
640             eprintln!(
641                 "[Error] Test '{}' time-out after {} seconds",
642                 test.name, test_timeout
643             );
644             Error::TestTimeout
645         })?
646 }
647 
648 fn date() -> String {
649     let output = test_infra::exec_host_command_output("date");
650     String::from_utf8_lossy(&output.stdout).trim().to_string()
651 }
652 
653 fn main() {
654     let cmd_arguments = ClapCommand::new("performance-metrics")
655         .version(env!("CARGO_PKG_VERSION"))
656         .author(env!("CARGO_PKG_AUTHORS"))
657         .about("Generate the performance metrics data for Cloud Hypervisor")
658         .arg(
659             Arg::new("test-filter")
660                 .long("test-filter")
661                 .help("Filter metrics tests to run based on provided keywords")
662                 .num_args(1)
663                 .required(false),
664         )
665         .arg(
666             Arg::new("list-tests")
667                 .long("list-tests")
668                 .help("Print the list of available metrics tests")
669                 .num_args(0)
670                 .action(ArgAction::SetTrue)
671                 .required(false),
672         )
673         .arg(
674             Arg::new("report-file")
675                 .long("report-file")
676                 .help("Report file. Standard error is used if not specified")
677                 .num_args(1),
678         )
679         .arg(
680             Arg::new("iterations")
681                 .long("iterations")
682                 .help("Override number of test iterations")
683                 .num_args(1),
684         )
685         .arg(
686             Arg::new("timeout")
687                 .long("timeout")
688                 .help("Override test timeout, Ex. --timeout 5")
689                 .num_args(1),
690         )
691         .get_matches();
692 
693     // It seems that the tool (ethr) used for testing the virtio-net latency
694     // is not stable on AArch64, and therefore the latency test is currently
695     // skipped on AArch64.
696     let test_list: Vec<&PerformanceTest> = TEST_LIST
697         .iter()
698         .filter(|t| !(cfg!(target_arch = "aarch64") && t.name == "virtio_net_latency_us"))
699         .collect();
700 
701     if cmd_arguments.get_flag("list-tests") {
702         for test in test_list.iter() {
703             println!("\"{}\" ({})", test.name, test.control);
704         }
705 
706         return;
707     }
708 
709     let test_filter = match cmd_arguments.get_many::<String>("test-filter") {
710         Some(s) => s.collect(),
711         None => Vec::new(),
712     };
713 
714     // Run performance tests sequentially and report results (in both readable/json format)
715     let mut metrics_report: MetricsReport = Default::default();
716 
717     init_tests();
718 
719     let overrides = Arc::new(PerformanceTestOverrides {
720         test_iterations: cmd_arguments
721             .get_one::<String>("iterations")
722             .map(|s| s.parse())
723             .transpose()
724             .unwrap_or_default(),
725         test_timeout: cmd_arguments
726             .get_one::<String>("timeout")
727             .map(|s| s.parse())
728             .transpose()
729             .unwrap_or_default(),
730     });
731 
732     for test in test_list.iter() {
733         if test_filter.is_empty() || test_filter.iter().any(|&s| test.name.contains(s)) {
734             match run_test_with_timeout(test, &overrides) {
735                 Ok(r) => {
736                     metrics_report.results.push(r);
737                 }
738                 Err(e) => {
739                     eprintln!("Aborting test due to error: '{e:?}'");
740                     std::process::exit(1);
741                 }
742             };
743         }
744     }
745 
746     cleanup_tests();
747 
748     let mut report_file: Box<dyn std::io::Write + Send> =
749         if let Some(file) = cmd_arguments.get_one::<String>("report-file") {
750             Box::new(
751                 std::fs::File::create(std::path::Path::new(file))
752                     .map_err(|e| {
753                         eprintln!("Error opening report file: {file}: {e}");
754                         std::process::exit(1);
755                     })
756                     .unwrap(),
757             )
758         } else {
759             Box::new(std::io::stdout())
760         };
761 
762     report_file
763         .write_all(
764             serde_json::to_string_pretty(&metrics_report)
765                 .unwrap()
766                 .as_bytes(),
767         )
768         .map_err(|e| {
769             eprintln!("Error writing report file: {e}");
770             std::process::exit(1);
771         })
772         .unwrap();
773 }
774