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