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