1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::fmt::{Display, Formatter};
4use std::num::ParseIntError;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use bytesize::ByteSize;
9use chrono::serde::ts_milliseconds;
10use chrono::serde::ts_seconds;
11use chrono::{DateTime, Utc};
12use regex::Regex;
13use serde_derive::Deserialize;
14use serde_derive::Serialize;
15use serde_with::DisplayFromStr;
16use serde_with::serde_as;
17use thiserror::Error;
18
19#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct FioResult {
22 #[serde(rename = "fio version")]
23 pub fio_version: String,
24 #[serde(with = "ts_seconds")]
25 pub timestamp: DateTime<Utc>,
26 #[serde(rename = "timestamp_ms", with = "ts_milliseconds")]
27 pub timestamp_ms: DateTime<Utc>,
28 pub time: String,
29 #[serde(rename = "global options")]
30 pub global_options: GlobalOptions,
31 pub jobs: Vec<Job>,
32 #[serde(rename = "disk_util")]
33 pub disk_util: Vec<DiskUtil>,
34}
35
36#[serde_as]
37#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct GlobalOptions {
40 pub filename: PathBuf,
41 pub size: ByteSize,
42 #[serde(rename = "cpus_allowed")]
43 pub cpus_allowed: String,
44 #[serde(rename = "cpus_allowed_policy")]
45 pub cpus_allowed_policy: String,
46 pub ioscheduler: String,
47 pub direct: String,
48 pub rw: TestType,
49 #[serde_as(as = "DisplayFromStr")]
50 pub bs: BlockSize,
51 pub ioengine: String,
52 pub iodepth: String,
53 pub runtime: String,
54 pub numjobs: String,
55}
56
57#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct Job {
60 pub jobname: String,
61 pub groupid: u64,
62 #[serde(rename = "job_start", with = "ts_milliseconds")]
63 pub job_start: DateTime<Utc>,
64 pub error: u64,
65 pub eta: u64,
66 pub elapsed: u64,
67 #[serde(rename = "job options")]
68 pub job_options: JobOptions,
69 pub read: Read,
70 pub write: Write,
71 pub trim: Trim,
72 pub sync: Sync,
73 #[serde(rename = "job_runtime")]
74 pub job_runtime: u64,
75 #[serde(rename = "usr_cpu")]
76 pub usr_cpu: f64,
77 #[serde(rename = "sys_cpu")]
78 pub sys_cpu: f64,
79 pub ctx: u64,
80 pub majf: u64,
81 pub minf: u64,
82 #[serde(rename = "iodepth_level")]
83 pub iodepth_level: IodepthLevel,
84 #[serde(rename = "iodepth_submit")]
85 pub iodepth_submit: IodepthSubmit,
86 #[serde(rename = "iodepth_complete")]
87 pub iodepth_complete: IodepthComplete,
88 #[serde(rename = "latency_ns")]
89 pub latency_ns: LatencyNs,
90 #[serde(rename = "latency_us")]
91 pub latency_us: LatencyUs,
92 #[serde(rename = "latency_ms")]
93 pub latency_ms: LatencyMs,
94 #[serde(rename = "latency_depth")]
95 pub latency_depth: u64,
96 #[serde(rename = "latency_target")]
97 pub latency_target: u64,
98 #[serde(rename = "latency_percentile")]
99 pub latency_percentile: f64,
100 #[serde(rename = "latency_window")]
101 pub latency_window: u64,
102}
103
104#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct JobOptions {
107 pub name: String,
108}
109
110#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct Read {
113 #[serde(rename = "io_bytes")]
114 pub io_bytes: ByteSize,
115 #[serde(rename = "io_kbytes")]
116 pub io_kbytes: u64, #[serde(rename = "bw_bytes")]
118 pub bw_bytes: ByteSize,
119 pub bw: u64,
120 pub iops: f64,
121 pub runtime: u64,
122 #[serde(rename = "total_ios")]
123 pub total_ios: u64,
124 #[serde(rename = "short_ios")]
125 pub short_ios: u64,
126 #[serde(rename = "drop_ios")]
127 pub drop_ios: u64,
128 #[serde(rename = "slat_ns")]
129 pub slat_ns: SlatNs,
130 #[serde(rename = "clat_ns")]
131 pub clat_ns: ClatNs,
132 #[serde(rename = "lat_ns")]
133 pub lat_ns: LatNs,
134 #[serde(rename = "bw_min")]
135 pub bw_min: u64,
136 #[serde(rename = "bw_max")]
137 pub bw_max: u64,
138 #[serde(rename = "bw_agg")]
139 pub bw_agg: f64,
140 #[serde(rename = "bw_mean")]
141 pub bw_mean: f64,
142 #[serde(rename = "bw_dev")]
143 pub bw_dev: f64,
144 #[serde(rename = "bw_samples")]
145 pub bw_samples: u64,
146 #[serde(rename = "iops_min")]
147 pub iops_min: u64,
148 #[serde(rename = "iops_max")]
149 pub iops_max: u64,
150 #[serde(rename = "iops_mean")]
151 pub iops_mean: f64,
152 #[serde(rename = "iops_stddev")]
153 pub iops_stddev: f64,
154 #[serde(rename = "iops_samples")]
155 pub iops_samples: u64,
156}
157
158#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct SlatNs {
161 pub min: u64,
162 pub max: u64,
163 pub mean: f64,
164 pub stddev: f64,
165 #[serde(rename = "N")]
166 pub n: u64,
167}
168
169#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct ClatNs {
172 pub min: u64,
173 pub max: u64,
174 pub mean: f64,
175 pub stddev: f64,
176 #[serde(rename = "N")]
177 pub n: u64,
178 pub percentile: Percentile,
179}
180
181#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct Percentile {
184 #[serde(rename = "1.000000")]
185 pub n1_000000: u64,
186 #[serde(rename = "5.000000")]
187 pub n5_000000: u64,
188 #[serde(rename = "10.000000")]
189 pub n10_000000: u64,
190 #[serde(rename = "20.000000")]
191 pub n20_000000: u64,
192 #[serde(rename = "30.000000")]
193 pub n30_000000: u64,
194 #[serde(rename = "40.000000")]
195 pub n40_000000: u64,
196 #[serde(rename = "50.000000")]
197 pub n50_000000: u64,
198 #[serde(rename = "60.000000")]
199 pub n60_000000: u64,
200 #[serde(rename = "70.000000")]
201 pub n70_000000: u64,
202 #[serde(rename = "80.000000")]
203 pub n80_000000: u64,
204 #[serde(rename = "90.000000")]
205 pub n90_000000: u64,
206 #[serde(rename = "95.000000")]
207 pub n95_000000: u64,
208 #[serde(rename = "99.000000")]
209 pub n99_000000: u64,
210 #[serde(rename = "99.500000")]
211 pub n99_500000: u64,
212 #[serde(rename = "99.900000")]
213 pub n99_900000: u64,
214 #[serde(rename = "99.950000")]
215 pub n99_950000: u64,
216 #[serde(rename = "99.990000")]
217 pub n99_990000: u64,
218}
219
220#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct LatNs {
223 pub min: u64,
224 pub max: u64,
225 pub mean: f64,
226 pub stddev: f64,
227 #[serde(rename = "N")]
228 pub n: u64,
229}
230
231#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct Write {
234 #[serde(rename = "io_bytes")]
235 pub io_bytes: ByteSize,
236 #[serde(rename = "io_kbytes")]
237 pub io_kbytes: u64,
238 #[serde(rename = "bw_bytes")]
239 pub bw_bytes: ByteSize,
240 pub bw: u64,
241 pub iops: f64,
242 pub runtime: u64,
243 #[serde(rename = "total_ios")]
244 pub total_ios: u64,
245 #[serde(rename = "short_ios")]
246 pub short_ios: u64,
247 #[serde(rename = "drop_ios")]
248 pub drop_ios: u64,
249 #[serde(rename = "slat_ns")]
250 pub slat_ns: SlatNs2,
251 #[serde(rename = "clat_ns")]
252 pub clat_ns: ClatNs2,
253 #[serde(rename = "lat_ns")]
254 pub lat_ns: LatNs2,
255 #[serde(rename = "bw_min")]
256 pub bw_min: u64,
257 #[serde(rename = "bw_max")]
258 pub bw_max: u64,
259 #[serde(rename = "bw_agg")]
260 pub bw_agg: f64,
261 #[serde(rename = "bw_mean")]
262 pub bw_mean: f64,
263 #[serde(rename = "bw_dev")]
264 pub bw_dev: f64,
265 #[serde(rename = "bw_samples")]
266 pub bw_samples: u64,
267 #[serde(rename = "iops_min")]
268 pub iops_min: u64,
269 #[serde(rename = "iops_max")]
270 pub iops_max: u64,
271 #[serde(rename = "iops_mean")]
272 pub iops_mean: f64,
273 #[serde(rename = "iops_stddev")]
274 pub iops_stddev: f64,
275 #[serde(rename = "iops_samples")]
276 pub iops_samples: u64,
277}
278
279#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct SlatNs2 {
282 pub min: u64,
283 pub max: u64,
284 pub mean: f64,
285 pub stddev: f64,
286 #[serde(rename = "N")]
287 pub n: u64,
288}
289
290#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct ClatNs2 {
293 pub min: u64,
294 pub max: u64,
295 pub mean: f64,
296 pub stddev: f64,
297 #[serde(rename = "N")]
298 pub n: u64,
299}
300
301#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct LatNs2 {
304 pub min: u64,
305 pub max: u64,
306 pub mean: f64,
307 pub stddev: f64,
308 #[serde(rename = "N")]
309 pub n: u64,
310}
311
312#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct Trim {
315 #[serde(rename = "io_bytes")]
316 pub io_bytes: ByteSize,
317 #[serde(rename = "io_kbytes")]
318 pub io_kbytes: u64, #[serde(rename = "bw_bytes")]
320 pub bw_bytes: ByteSize,
321 pub bw: u64,
322 pub iops: f64,
323 pub runtime: u64,
324 #[serde(rename = "total_ios")]
325 pub total_ios: u64,
326 #[serde(rename = "short_ios")]
327 pub short_ios: u64,
328 #[serde(rename = "drop_ios")]
329 pub drop_ios: u64,
330 #[serde(rename = "slat_ns")]
331 pub slat_ns: SlatNs3,
332 #[serde(rename = "clat_ns")]
333 pub clat_ns: ClatNs3,
334 #[serde(rename = "lat_ns")]
335 pub lat_ns: LatNs3,
336 #[serde(rename = "bw_min")]
337 pub bw_min: u64,
338 #[serde(rename = "bw_max")]
339 pub bw_max: u64,
340 #[serde(rename = "bw_agg")]
341 pub bw_agg: f64,
342 #[serde(rename = "bw_mean")]
343 pub bw_mean: f64,
344 #[serde(rename = "bw_dev")]
345 pub bw_dev: f64,
346 #[serde(rename = "bw_samples")]
347 pub bw_samples: u64,
348 #[serde(rename = "iops_min")]
349 pub iops_min: u64,
350 #[serde(rename = "iops_max")]
351 pub iops_max: u64,
352 #[serde(rename = "iops_mean")]
353 pub iops_mean: f64,
354 #[serde(rename = "iops_stddev")]
355 pub iops_stddev: f64,
356 #[serde(rename = "iops_samples")]
357 pub iops_samples: u64,
358}
359
360#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct SlatNs3 {
363 pub min: u64,
364 pub max: u64,
365 pub mean: f64,
366 pub stddev: f64,
367 #[serde(rename = "N")]
368 pub n: u64,
369}
370
371#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct ClatNs3 {
374 pub min: u64,
375 pub max: u64,
376 pub mean: f64,
377 pub stddev: f64,
378 #[serde(rename = "N")]
379 pub n: u64,
380}
381
382#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct LatNs3 {
385 pub min: u64,
386 pub max: u64,
387 pub mean: f64,
388 pub stddev: f64,
389 #[serde(rename = "N")]
390 pub n: u64,
391}
392
393#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct Sync {
396 #[serde(rename = "total_ios")]
397 pub total_ios: u64,
398 #[serde(rename = "lat_ns")]
399 pub lat_ns: LatNs4,
400}
401
402#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct LatNs4 {
405 pub min: u64,
406 pub max: u64,
407 pub mean: f64,
408 pub stddev: f64,
409 #[serde(rename = "N")]
410 pub n: u64,
411}
412
413#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct IodepthLevel {
416 #[serde(rename = "1")]
417 pub n1: f64,
418 #[serde(rename = "2")]
419 pub n2: f64,
420 #[serde(rename = "4")]
421 pub n4: f64,
422 #[serde(rename = "8")]
423 pub n8: f64,
424 #[serde(rename = "16")]
425 pub n16: f64,
426 #[serde(rename = "32")]
427 pub n32: f64,
428 #[serde(rename = ">=64")]
429 pub n64: f64,
430}
431
432#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
433#[serde(rename_all = "camelCase")]
434pub struct IodepthSubmit {
435 #[serde(rename = "0")]
436 pub n0: f64,
437 #[serde(rename = "4")]
438 pub n4: f64,
439 #[serde(rename = "8")]
440 pub n8: f64,
441 #[serde(rename = "16")]
442 pub n16: f64,
443 #[serde(rename = "32")]
444 pub n32: f64,
445 #[serde(rename = "64")]
446 pub n64: f64,
447 #[serde(rename = ">=64")]
448 pub n642: f64,
449}
450
451#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct IodepthComplete {
454 #[serde(rename = "0")]
455 pub n0: f64,
456 #[serde(rename = "4")]
457 pub n4: f64,
458 #[serde(rename = "8")]
459 pub n8: f64,
460 #[serde(rename = "16")]
461 pub n16: f64,
462 #[serde(rename = "32")]
463 pub n32: f64,
464 #[serde(rename = "64")]
465 pub n64: f64,
466 #[serde(rename = ">=64")]
467 pub n642: f64,
468}
469
470#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct LatencyNs {
473 #[serde(rename = "2")]
474 pub n2: f64,
475 #[serde(rename = "4")]
476 pub n4: f64,
477 #[serde(rename = "10")]
478 pub n10: f64,
479 #[serde(rename = "20")]
480 pub n20: f64,
481 #[serde(rename = "50")]
482 pub n50: f64,
483 #[serde(rename = "100")]
484 pub n100: f64,
485 #[serde(rename = "250")]
486 pub n250: f64,
487 #[serde(rename = "500")]
488 pub n500: f64,
489 #[serde(rename = "750")]
490 pub n750: f64,
491 #[serde(rename = "1000")]
492 pub n1000: f64,
493}
494
495#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct LatencyUs {
498 #[serde(rename = "2")]
499 pub n2: f64,
500 #[serde(rename = "4")]
501 pub n4: f64,
502 #[serde(rename = "10")]
503 pub n10: f64,
504 #[serde(rename = "20")]
505 pub n20: f64,
506 #[serde(rename = "50")]
507 pub n50: f64,
508 #[serde(rename = "100")]
509 pub n100: f64,
510 #[serde(rename = "250")]
511 pub n250: f64,
512 #[serde(rename = "500")]
513 pub n500: f64,
514 #[serde(rename = "750")]
515 pub n750: f64,
516 #[serde(rename = "1000")]
517 pub n1000: f64,
518}
519
520#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
521#[serde(rename_all = "camelCase")]
522pub struct LatencyMs {
523 #[serde(rename = "2")]
524 pub n2: f64,
525 #[serde(rename = "4")]
526 pub n4: f64,
527 #[serde(rename = "10")]
528 pub n10: f64,
529 #[serde(rename = "20")]
530 pub n20: f64,
531 #[serde(rename = "50")]
532 pub n50: f64,
533 #[serde(rename = "100")]
534 pub n100: f64,
535 #[serde(rename = "250")]
536 pub n250: f64,
537 #[serde(rename = "500")]
538 pub n500: f64,
539 #[serde(rename = "750")]
540 pub n750: f64,
541 #[serde(rename = "1000")]
542 pub n1000: f64,
543 #[serde(rename = "2000")]
544 pub n2000: f64,
545 #[serde(rename = ">=2000")]
546 pub n20002: f64,
547}
548
549#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct DiskUtil {
552 pub name: String,
553 #[serde(rename = "read_ios")]
554 pub read_ios: u64,
555 #[serde(rename = "write_ios")]
556 pub write_ios: u64,
557 #[serde(rename = "read_sectors")]
558 pub read_sectors: u64,
559 #[serde(rename = "write_sectors")]
560 pub write_sectors: u64,
561 #[serde(rename = "read_merges")]
562 pub read_merges: u64,
563 #[serde(rename = "write_merges")]
564 pub write_merges: u64,
565 #[serde(rename = "read_ticks")]
566 pub read_ticks: u64,
567 #[serde(rename = "write_ticks")]
568 pub write_ticks: u64,
569 #[serde(rename = "in_queue")]
570 pub in_queue: u64,
571 pub util: f64,
572}
573
574#[derive(Default, Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
575pub enum TestType {
576 #[default]
577 #[serde(rename = "read")]
578 Read,
579 #[serde(rename = "write")]
580 Write,
581 #[serde(rename = "trim")]
582 Trim,
583 #[serde(rename = "randread")]
584 RandRead,
585 #[serde(rename = "randwrite")]
586 RandWrite,
587 #[serde(rename = "randtrim")]
588 RandTrim,
589 #[serde(rename = "rw,readwrite")]
590 RwReadWrite,
591 #[serde(rename = "randrw")]
592 RandRw,
593 #[serde(rename = "trimwrite")]
594 TrimWrite,
595 #[serde(rename = "randtrimwrite")]
596 RandTrimWrite,
597}
598
599impl Display for TestType {
600 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
601 match self {
602 TestType::Read => write!(f, "read"),
603 TestType::Write => write!(f, "write"),
604 TestType::Trim => write!(f, "trim"),
605 TestType::RandRead => write!(f, "randread"),
606 TestType::RandWrite => write!(f, "randwrite"),
607 TestType::RandTrim => write!(f, "randtrim"),
608 TestType::RwReadWrite => write!(f, "rw_readwrite"),
609 TestType::RandRw => write!(f, "randrw"),
610 TestType::TrimWrite => write!(f, "trimwrite"),
611 TestType::RandTrimWrite => write!(f, "randtrimwrite"),
612 }
613 }
614}
615#[derive(Debug, Error)]
616pub enum FioResultError {
617 #[error("i/o {0}")]
618 Io(#[from] std::io::Error),
619
620 #[error("json {0}")]
621 Json(#[from] serde_json::Error),
622}
623
624#[derive(Default, Debug, Clone, Eq, Deserialize, Serialize)]
625pub struct BlockSize {
626 orig: String,
627 bs: ByteSize,
628}
629
630#[derive(Debug, Error)]
631pub enum ParseBlockSizeError {
632 #[error("{0}")]
633 Int(#[from] ParseIntError),
634
635 #[error("unknown format")]
636 UnknownFormat,
637}
638
639impl FromStr for BlockSize {
640 type Err = ParseBlockSizeError;
641
642 fn from_str(s: &str) -> Result<Self, Self::Err> {
643 let re = Regex::new(r"^([0-9]+)([kKG]?)$").unwrap();
644 if let Some((_, [amount, unit])) = re.captures_iter(s).map(|c| c.extract()).next() {
645 let a = amount.parse::<u64>()?;
646 match unit {
647 "K" | "k" => {
648 return Ok(BlockSize {
649 orig: s.to_string(),
650 bs: ByteSize::kb(bytesize::kb(a)),
651 });
652 }
653 _ => return Err(ParseBlockSizeError::UnknownFormat),
654 }
655 }
656
657 Err(ParseBlockSizeError::UnknownFormat)
658 }
659}
660
661impl Display for BlockSize {
662 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
663 self.orig.fmt(f)
664 }
665}
666impl PartialEq for BlockSize {
667 fn eq(&self, other: &Self) -> bool {
668 self.bs == other.bs
669 }
670}
671
672impl Ord for BlockSize {
673 fn cmp(&self, other: &Self) -> Ordering {
674 self.bs.cmp(&other.bs)
675 }
676}
677
678impl PartialOrd for BlockSize {
679 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
680 Some(self.cmp(other))
681 }
682}
683
684pub fn parse_file<P: AsRef<Path>>(filepath: P) -> Result<FioResult, FioResultError> {
685 let contents = std::fs::read_to_string(&filepath)?;
686
687 let deserialized: FioResult = serde_json::from_str(&contents)?;
688
689 Ok(deserialized)
690}
691
692pub type IoDepth = String;
693
694pub type GroupByTestType = BTreeMap<(TestType, BlockSize), BTreeMap<IoDepth, FioResult>>;
695pub type MergeByTestType =
696 BTreeMap<(TestType, BlockSize), BTreeMap<IoDepth, (FioResult, FioResult)>>;