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