fio_results/
lib.rs

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, // TODO
117    #[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, // TODO
319    #[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)>>;