xref: /cloud-hypervisor/performance-metrics/src/performance_tests.rs (revision 190a11f2124b0b60a2d44e85b7c9988373acfb6d)
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")]
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