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