1 // Copyright © 2022 Intel Corporation 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 6 // Performance tests 7 8 use std::path::PathBuf; 9 use std::time::Duration; 10 use std::{fs, thread}; 11 12 use test_infra::{Error as InfraError, *}; 13 14 use crate::{mean, PerformanceTestControl}; 15 16 #[cfg(target_arch = "x86_64")] 17 pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-amd64-custom-20210609-0.raw"; 18 #[cfg(target_arch = "aarch64")] 19 pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-arm64-custom-20210929-0-update-tool.raw"; 20 21 #[allow(dead_code)] 22 #[derive(Debug)] 23 enum Error { 24 BootTimeParse, 25 Infra(InfraError), 26 RestoreTimeParse, 27 } 28 29 impl From<InfraError> for Error { 30 fn from(e: InfraError) -> Self { 31 Self::Infra(e) 32 } 33 } 34 35 const BLK_IO_TEST_IMG: &str = "/var/tmp/ch-blk-io-test.img"; 36 37 pub fn init_tests() { 38 // The test image cannot be created on tmpfs (e.g. /tmp) filesystem, 39 // as tmpfs does not support O_DIRECT 40 assert!(exec_host_command_output(&format!( 41 "dd if=/dev/zero of={BLK_IO_TEST_IMG} bs=1M count=4096" 42 )) 43 .status 44 .success()); 45 } 46 47 pub fn cleanup_tests() { 48 fs::remove_file(BLK_IO_TEST_IMG) 49 .unwrap_or_else(|_| panic!("Failed to remove file '{BLK_IO_TEST_IMG}'.")); 50 } 51 52 // Performance tests are expected to be executed sequentially, so we can 53 // start VM guests with the same IP while putting them on a different 54 // private network. The default constructor "Guest::new()" does not work 55 // well, as we can easily create more than 256 VMs from repeating various 56 // performance tests dozens times in a single run. 57 fn performance_test_new_guest(disk_config: Box<dyn DiskConfig>) -> Guest { 58 Guest::new_from_ip_range(disk_config, "172.19", 0) 59 } 60 61 const DIRECT_KERNEL_BOOT_CMDLINE: &str = 62 "root=/dev/vda1 console=hvc0 rw systemd.journald.forward_to_console=1"; 63 64 // Creates the path for direct kernel boot and return the path. 65 // For x86_64, this function returns the vmlinux kernel path. 66 // For AArch64, this function returns the PE kernel path. 67 fn direct_kernel_boot_path() -> PathBuf { 68 let mut workload_path = dirs::home_dir().unwrap(); 69 workload_path.push("workloads"); 70 71 let mut kernel_path = workload_path; 72 #[cfg(target_arch = "x86_64")] 73 kernel_path.push("vmlinux"); 74 #[cfg(target_arch = "aarch64")] 75 kernel_path.push("Image"); 76 77 kernel_path 78 } 79 80 fn remote_command(api_socket: &str, command: &str, arg: Option<&str>) -> bool { 81 let mut cmd = std::process::Command::new(clh_command("ch-remote")); 82 cmd.args([&format!("--api-socket={api_socket}"), command]); 83 84 if let Some(arg) = arg { 85 cmd.arg(arg); 86 } 87 let output = cmd.output().unwrap(); 88 if output.status.success() { 89 true 90 } else { 91 eprintln!( 92 "Error running ch-remote command: {:?}\nstderr: {}", 93 &cmd, 94 String::from_utf8_lossy(&output.stderr) 95 ); 96 false 97 } 98 } 99 100 pub fn performance_net_throughput(control: &PerformanceTestControl) -> f64 { 101 let test_timeout = control.test_timeout; 102 let (rx, bandwidth) = control.net_control.unwrap(); 103 104 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 105 let guest = performance_test_new_guest(Box::new(focal)); 106 107 let num_queues = control.num_queues.unwrap(); 108 let queue_size = control.queue_size.unwrap(); 109 let net_params = format!( 110 "tap=,mac={},ip={},mask=255.255.255.0,num_queues={},queue_size={}", 111 guest.network.guest_mac, guest.network.host_ip, num_queues, queue_size, 112 ); 113 114 let mut child = GuestCommand::new(&guest) 115 .args(["--cpus", &format!("boot={num_queues}")]) 116 .args(["--memory", "size=4G"]) 117 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 118 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 119 .default_disks() 120 .args(["--net", net_params.as_str()]) 121 .capture_output() 122 .verbosity(VerbosityLevel::Warn) 123 .set_print_cmd(false) 124 .spawn() 125 .unwrap(); 126 127 let r = std::panic::catch_unwind(|| { 128 guest.wait_vm_boot(None).unwrap(); 129 measure_virtio_net_throughput(test_timeout, num_queues / 2, &guest, rx, bandwidth).unwrap() 130 }); 131 132 let _ = child.kill(); 133 let output = child.wait_with_output().unwrap(); 134 135 match r { 136 Ok(r) => r, 137 Err(e) => { 138 handle_child_output(Err(e), &output); 139 panic!("test failed!"); 140 } 141 } 142 } 143 144 pub fn performance_net_latency(control: &PerformanceTestControl) -> f64 { 145 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 146 let guest = performance_test_new_guest(Box::new(focal)); 147 148 let num_queues = control.num_queues.unwrap(); 149 let queue_size = control.queue_size.unwrap(); 150 let net_params = format!( 151 "tap=,mac={},ip={},mask=255.255.255.0,num_queues={},queue_size={}", 152 guest.network.guest_mac, guest.network.host_ip, num_queues, queue_size, 153 ); 154 155 let mut child = GuestCommand::new(&guest) 156 .args(["--cpus", &format!("boot={num_queues}")]) 157 .args(["--memory", "size=4G"]) 158 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 159 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 160 .default_disks() 161 .args(["--net", net_params.as_str()]) 162 .capture_output() 163 .verbosity(VerbosityLevel::Warn) 164 .set_print_cmd(false) 165 .spawn() 166 .unwrap(); 167 168 let r = std::panic::catch_unwind(|| { 169 guest.wait_vm_boot(None).unwrap(); 170 171 // 'ethr' tool will measure the latency multiple times with provided test time 172 let latency = measure_virtio_net_latency(&guest, control.test_timeout).unwrap(); 173 mean(&latency).unwrap() 174 }); 175 176 let _ = child.kill(); 177 let output = child.wait_with_output().unwrap(); 178 179 match r { 180 Ok(r) => r, 181 Err(e) => { 182 handle_child_output(Err(e), &output); 183 panic!("test failed!"); 184 } 185 } 186 } 187 188 fn parse_boot_time_output(output: &[u8]) -> Result<f64, Error> { 189 std::panic::catch_unwind(|| { 190 let l: Vec<String> = String::from_utf8_lossy(output) 191 .lines() 192 .filter(|l| l.contains("Debug I/O port: Kernel code")) 193 .map(|l| l.to_string()) 194 .collect(); 195 196 assert_eq!( 197 l.len(), 198 2, 199 "Expecting two matching lines for 'Debug I/O port: Kernel code'" 200 ); 201 202 let time_stamp_kernel_start = { 203 let s = l[0].split("--").collect::<Vec<&str>>(); 204 assert_eq!( 205 s.len(), 206 2, 207 "Expecting '--' for the matching line of 'Debug I/O port' output" 208 ); 209 210 // Sample output: "[Debug I/O port: Kernel code 0x40] 0.096537 seconds" 211 assert!( 212 s[1].contains("0x40"), 213 "Expecting kernel code '0x40' for 'linux_kernel_start' time stamp output" 214 ); 215 let t = s[1].split_whitespace().collect::<Vec<&str>>(); 216 assert_eq!( 217 t.len(), 218 8, 219 "Expecting exact '8' words from the 'Debug I/O port' output" 220 ); 221 assert!( 222 t[7].eq("seconds"), 223 "Expecting 'seconds' as the last word of the 'Debug I/O port' output" 224 ); 225 226 t[6].parse::<f64>().unwrap() 227 }; 228 229 let time_stamp_user_start = { 230 let s = l[1].split("--").collect::<Vec<&str>>(); 231 assert_eq!( 232 s.len(), 233 2, 234 "Expecting '--' for the matching line of 'Debug I/O port' output" 235 ); 236 237 // Sample output: "Debug I/O port: Kernel code 0x41] 0.198980 seconds" 238 assert!( 239 s[1].contains("0x41"), 240 "Expecting kernel code '0x41' for 'linux_kernel_start' time stamp output" 241 ); 242 let t = s[1].split_whitespace().collect::<Vec<&str>>(); 243 assert_eq!( 244 t.len(), 245 8, 246 "Expecting exact '8' words from the 'Debug I/O port' output" 247 ); 248 assert!( 249 t[7].eq("seconds"), 250 "Expecting 'seconds' as the last word of the 'Debug I/O port' output" 251 ); 252 253 t[6].parse::<f64>().unwrap() 254 }; 255 256 time_stamp_user_start - time_stamp_kernel_start 257 }) 258 .map_err(|_| { 259 eprintln!( 260 "=============== boot-time output ===============\n\n{}\n\n===========end============\n\n", 261 String::from_utf8_lossy(output) 262 ); 263 Error::BootTimeParse 264 }) 265 } 266 267 fn measure_boot_time(cmd: &mut GuestCommand, test_timeout: u32) -> Result<f64, Error> { 268 let mut child = cmd 269 .capture_output() 270 .verbosity(VerbosityLevel::Warn) 271 .set_print_cmd(false) 272 .spawn() 273 .unwrap(); 274 275 thread::sleep(Duration::new(test_timeout as u64, 0)); 276 let _ = child.kill(); 277 let output = child.wait_with_output().unwrap(); 278 279 parse_boot_time_output(&output.stderr).inspect_err(|_| { 280 eprintln!( 281 "\n\n==== Start child stdout ====\n\n{}\n\n==== End child stdout ====", 282 String::from_utf8_lossy(&output.stdout) 283 ); 284 eprintln!( 285 "\n\n==== Start child stderr ====\n\n{}\n\n==== End child stderr ====", 286 String::from_utf8_lossy(&output.stderr) 287 ); 288 }) 289 } 290 291 pub fn performance_boot_time(control: &PerformanceTestControl) -> f64 { 292 let r = std::panic::catch_unwind(|| { 293 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 294 let guest = performance_test_new_guest(Box::new(focal)); 295 let mut cmd = GuestCommand::new(&guest); 296 297 let c = cmd 298 .args([ 299 "--cpus", 300 &format!("boot={}", control.num_boot_vcpus.unwrap_or(1)), 301 ]) 302 .args(["--memory", "size=1G"]) 303 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 304 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 305 .args(["--console", "off"]) 306 .default_disks(); 307 308 measure_boot_time(c, control.test_timeout).unwrap() 309 }); 310 311 match r { 312 Ok(r) => r, 313 Err(_) => { 314 panic!("test failed!"); 315 } 316 } 317 } 318 319 pub fn performance_boot_time_pmem(control: &PerformanceTestControl) -> f64 { 320 let r = std::panic::catch_unwind(|| { 321 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 322 let guest = performance_test_new_guest(Box::new(focal)); 323 let mut cmd = GuestCommand::new(&guest); 324 let c = cmd 325 .args([ 326 "--cpus", 327 &format!("boot={}", control.num_boot_vcpus.unwrap_or(1)), 328 ]) 329 .args(["--memory", "size=1G,hugepages=on"]) 330 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 331 .args(["--cmdline", "root=/dev/pmem0p1 console=ttyS0 quiet rw"]) 332 .args(["--console", "off"]) 333 .args([ 334 "--pmem", 335 format!( 336 "file={}", 337 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 338 ) 339 .as_str(), 340 ]); 341 342 measure_boot_time(c, control.test_timeout).unwrap() 343 }); 344 345 match r { 346 Ok(r) => r, 347 Err(_) => { 348 panic!("test failed!"); 349 } 350 } 351 } 352 353 pub fn performance_block_io(control: &PerformanceTestControl) -> f64 { 354 let test_timeout = control.test_timeout; 355 let num_queues = control.num_queues.unwrap(); 356 let (fio_ops, bandwidth) = control.fio_control.as_ref().unwrap(); 357 358 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 359 let guest = performance_test_new_guest(Box::new(focal)); 360 let api_socket = guest 361 .tmp_dir 362 .as_path() 363 .join("cloud-hypervisor.sock") 364 .to_str() 365 .unwrap() 366 .to_string(); 367 368 let mut child = GuestCommand::new(&guest) 369 .args(["--cpus", &format!("boot={num_queues}")]) 370 .args(["--memory", "size=4G"]) 371 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 372 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 373 .args([ 374 "--disk", 375 format!( 376 "path={}", 377 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 378 ) 379 .as_str(), 380 format!( 381 "path={}", 382 guest.disk_config.disk(DiskType::CloudInit).unwrap() 383 ) 384 .as_str(), 385 format!("path={BLK_IO_TEST_IMG}").as_str(), 386 ]) 387 .default_net() 388 .args(["--api-socket", &api_socket]) 389 .capture_output() 390 .verbosity(VerbosityLevel::Warn) 391 .set_print_cmd(false) 392 .spawn() 393 .unwrap(); 394 395 let r = std::panic::catch_unwind(|| { 396 guest.wait_vm_boot(None).unwrap(); 397 398 let fio_command = format!( 399 "sudo fio --filename=/dev/vdc --name=test --output-format=json \ 400 --direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \ 401 --rw={fio_ops} --runtime={test_timeout} --numjobs={num_queues}" 402 ); 403 let output = guest 404 .ssh_command(&fio_command) 405 .map_err(InfraError::SshCommand) 406 .unwrap(); 407 408 // Parse fio output 409 if *bandwidth { 410 parse_fio_output(&output, fio_ops, num_queues).unwrap() 411 } else { 412 parse_fio_output_iops(&output, fio_ops, num_queues).unwrap() 413 } 414 }); 415 416 let _ = child.kill(); 417 let output = child.wait_with_output().unwrap(); 418 419 match r { 420 Ok(r) => r, 421 Err(e) => { 422 handle_child_output(Err(e), &output); 423 panic!("test failed!"); 424 } 425 } 426 } 427 428 // Parse the event_monitor file based on the format that each event 429 // is followed by a double newline 430 fn parse_event_file(event_file: &str) -> Vec<serde_json::Value> { 431 let content = fs::read(event_file).unwrap(); 432 let mut ret = Vec::new(); 433 for entry in String::from_utf8_lossy(&content) 434 .trim() 435 .split("\n\n") 436 .collect::<Vec<&str>>() 437 { 438 ret.push(serde_json::from_str(entry).unwrap()); 439 } 440 ret 441 } 442 443 fn parse_restore_time_output(events: &[serde_json::Value]) -> Result<f64, Error> { 444 for entry in events.iter() { 445 if entry["event"].as_str().unwrap() == "restored" { 446 let duration = entry["timestamp"]["secs"].as_u64().unwrap() as f64 * 1_000f64 447 + entry["timestamp"]["nanos"].as_u64().unwrap() as f64 / 1_000_000f64; 448 return Ok(duration); 449 } 450 } 451 Err(Error::RestoreTimeParse) 452 } 453 454 fn measure_restore_time( 455 cmd: &mut GuestCommand, 456 event_file: &str, 457 test_timeout: u32, 458 ) -> Result<f64, Error> { 459 let mut child = cmd 460 .capture_output() 461 .verbosity(VerbosityLevel::Warn) 462 .set_print_cmd(false) 463 .spawn() 464 .unwrap(); 465 466 thread::sleep(Duration::new((test_timeout / 2) as u64, 0)); 467 let _ = child.kill(); 468 let output = child.wait_with_output().unwrap(); 469 470 let json_events = parse_event_file(event_file); 471 472 parse_restore_time_output(&json_events).inspect_err(|_| { 473 eprintln!( 474 "\n\n==== Start child stdout ====\n\n{}\n\n==== End child stdout ====\ 475 \n\n==== Start child stderr ====\n\n{}\n\n==== End child stderr ====", 476 String::from_utf8_lossy(&output.stdout), 477 String::from_utf8_lossy(&output.stderr) 478 ) 479 }) 480 } 481 482 pub fn performance_restore_latency(control: &PerformanceTestControl) -> f64 { 483 let r = std::panic::catch_unwind(|| { 484 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 485 let guest = performance_test_new_guest(Box::new(focal)); 486 let api_socket_source = String::from( 487 guest 488 .tmp_dir 489 .as_path() 490 .join("cloud-hypervisor.sock") 491 .to_str() 492 .unwrap(), 493 ); 494 495 let mut child = GuestCommand::new(&guest) 496 .args(["--api-socket", &api_socket_source]) 497 .args([ 498 "--cpus", 499 &format!("boot={}", control.num_boot_vcpus.unwrap_or(1)), 500 ]) 501 .args(["--memory", "size=256M"]) 502 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 503 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 504 .args(["--console", "off"]) 505 .default_disks() 506 .set_print_cmd(false) 507 .spawn() 508 .unwrap(); 509 510 thread::sleep(Duration::new((control.test_timeout / 2) as u64, 0)); 511 let snapshot_dir = String::from(guest.tmp_dir.as_path().join("snapshot").to_str().unwrap()); 512 std::fs::create_dir(&snapshot_dir).unwrap(); 513 assert!(remote_command(&api_socket_source, "pause", None)); 514 assert!(remote_command( 515 &api_socket_source, 516 "snapshot", 517 Some(format!("file://{snapshot_dir}").as_str()), 518 )); 519 520 let _ = child.kill(); 521 child.wait().unwrap(); 522 523 let event_path = String::from(guest.tmp_dir.as_path().join("event.json").to_str().unwrap()); 524 let mut cmd = GuestCommand::new(&guest); 525 let c = cmd 526 .args([ 527 "--restore", 528 format!("source_url=file://{snapshot_dir}").as_str(), 529 ]) 530 .args(["--event-monitor", format!("path={event_path}").as_str()]); 531 532 measure_restore_time(c, event_path.as_str(), control.test_timeout).unwrap() 533 }); 534 535 match r { 536 Ok(r) => r, 537 Err(_) => { 538 panic!("test failed!"); 539 } 540 } 541 } 542 543 #[cfg(test)] 544 mod tests { 545 use super::*; 546 547 #[test] 548 fn test_parse_iperf3_output() { 549 let output = r#" 550 { 551 "end": { 552 "sum_sent": { 553 "start": 0, 554 "end": 5.000196, 555 "seconds": 5.000196, 556 "bytes": 14973836248, 557 "bits_per_second": 23957198874.604115, 558 "retransmits": 0, 559 "sender": false 560 } 561 } 562 } 563 "#; 564 assert_eq!( 565 parse_iperf3_output(output.as_bytes(), true, true).unwrap(), 566 23957198874.604115 567 ); 568 569 let output = r#" 570 { 571 "end": { 572 "sum_received": { 573 "start": 0, 574 "end": 5.000626, 575 "seconds": 5.000626, 576 "bytes": 24703557800, 577 "bits_per_second": 39520744482.79, 578 "sender": true 579 } 580 } 581 } 582 "#; 583 assert_eq!( 584 parse_iperf3_output(output.as_bytes(), false, true).unwrap(), 585 39520744482.79 586 ); 587 let output = r#" 588 { 589 "end": { 590 "sum": { 591 "start": 0, 592 "end": 5.000036, 593 "seconds": 5.000036, 594 "bytes": 29944971264, 595 "bits_per_second": 47911877363.396217, 596 "jitter_ms": 0.0038609822983198556, 597 "lost_packets": 16, 598 "packets": 913848, 599 "lost_percent": 0.0017508382137948542, 600 "sender": true 601 } 602 } 603 } 604 "#; 605 assert_eq!( 606 parse_iperf3_output(output.as_bytes(), true, false).unwrap(), 607 182765.08409139456 608 ); 609 } 610 611 #[test] 612 fn test_parse_ethr_latency_output() { 613 let output = r#"{"Time":"2022-02-08T03:52:50Z","Title":"","Type":"INFO","Message":"Using destination: 192.168.249.2, ip: 192.168.249.2, port: 8888"} 614 {"Time":"2022-02-08T03:52:51Z","Title":"","Type":"INFO","Message":"Running latency test: 1000, 1"} 615 {"Time":"2022-02-08T03:52:51Z","Title":"","Type":"LatencyResult","RemoteAddr":"192.168.249.2","Protocol":"TCP","Avg":"80.712us","Min":"61.677us","P50":"257.014us","P90":"74.418us","P95":"107.283us","P99":"119.309us","P999":"142.100us","P9999":"216.341us","Max":"216.341us"} 616 {"Time":"2022-02-08T03:52:52Z","Title":"","Type":"LatencyResult","RemoteAddr":"192.168.249.2","Protocol":"TCP","Avg":"79.826us","Min":"55.129us","P50":"598.996us","P90":"73.849us","P95":"106.552us","P99":"122.152us","P999":"142.459us","P9999":"474.280us","Max":"474.280us"} 617 {"Time":"2022-02-08T03:52:53Z","Title":"","Type":"LatencyResult","RemoteAddr":"192.168.249.2","Protocol":"TCP","Avg":"78.239us","Min":"56.999us","P50":"396.820us","P90":"69.469us","P95":"115.421us","P99":"119.404us","P999":"130.158us","P9999":"258.686us","Max":"258.686us"}"#; 618 619 let ret = parse_ethr_latency_output(output.as_bytes()).unwrap(); 620 let reference = vec![80.712_f64, 79.826_f64, 78.239_f64]; 621 assert_eq!(ret, reference); 622 } 623 624 #[test] 625 fn test_parse_boot_time_output() { 626 let output = r#" 627 cloud-hypervisor: 161.167103ms: <vcpu0> INFO:vmm/src/vm.rs:392 -- [Debug I/O port: Kernel code 0x40] 0.132 seconds 628 cloud-hypervisor: 613.57361ms: <vcpu0> INFO:vmm/src/vm.rs:392 -- [Debug I/O port: Kernel code 0x41] 0.5845 seconds 629 "#; 630 631 assert_eq!(parse_boot_time_output(output.as_bytes()).unwrap(), 0.4525); 632 } 633 #[test] 634 fn test_parse_restore_time_output() { 635 let output = r#" 636 { 637 "timestamp": { 638 "secs": 0, 639 "nanos": 4664404 640 }, 641 "source": "virtio-device", 642 "event": "activated", 643 "properties": { 644 "id": "__rng" 645 } 646 } 647 648 { 649 "timestamp": { 650 "secs": 0, 651 "nanos": 5505133 652 }, 653 "source": "vm", 654 "event": "restored", 655 "properties": null 656 } 657 "#; 658 let mut ret = Vec::new(); 659 for entry in String::from(output) 660 .trim() 661 .split("\n\n") 662 .collect::<Vec<&str>>() 663 { 664 ret.push(serde_json::from_str(entry).unwrap()); 665 } 666 667 assert_eq!(parse_restore_time_output(&ret).unwrap(), 5.505133_f64); 668 } 669 #[test] 670 fn test_parse_fio_output() { 671 let output = r#" 672 { 673 "jobs" : [ 674 { 675 "read" : { 676 "io_bytes" : 1965273088, 677 "io_kbytes" : 1919212, 678 "bw_bytes" : 392976022, 679 "bw" : 383765, 680 "iops" : 95941.411718, 681 "runtime" : 5001, 682 "total_ios" : 479803, 683 "short_ios" : 0, 684 "drop_ios" : 0 685 } 686 } 687 ] 688 } 689 "#; 690 691 let bps = 1965273088_f64 / (5001_f64 / 1000_f64); 692 assert_eq!( 693 parse_fio_output(output, &FioOps::RandomRead, 1).unwrap(), 694 bps 695 ); 696 assert_eq!(parse_fio_output(output, &FioOps::Read, 1).unwrap(), bps); 697 698 let output = r#" 699 { 700 "jobs" : [ 701 { 702 "write" : { 703 "io_bytes" : 1172783104, 704 "io_kbytes" : 1145296, 705 "bw_bytes" : 234462835, 706 "bw" : 228967, 707 "iops" : 57241.903239, 708 "runtime" : 5002, 709 "total_ios" : 286324, 710 "short_ios" : 0, 711 "drop_ios" : 0 712 } 713 }, 714 { 715 "write" : { 716 "io_bytes" : 1172234240, 717 "io_kbytes" : 1144760, 718 "bw_bytes" : 234353106, 719 "bw" : 228860, 720 "iops" : 57215.113954, 721 "runtime" : 5002, 722 "total_ios" : 286190, 723 "short_ios" : 0, 724 "drop_ios" : 0 725 } 726 } 727 ] 728 } 729 "#; 730 731 let bps = 1172783104_f64 / (5002_f64 / 1000_f64) + 1172234240_f64 / (5002_f64 / 1000_f64); 732 assert_eq!( 733 parse_fio_output(output, &FioOps::RandomWrite, 2).unwrap(), 734 bps 735 ); 736 assert_eq!(parse_fio_output(output, &FioOps::Write, 2).unwrap(), bps); 737 } 738 } 739