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