xref: /cloud-hypervisor/performance-metrics/src/performance_tests.rs (revision 88a9f799449c04180c6b9a21d3b9c0c4b57e2bd6)
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