Skip to main content

libcgroups/
stats.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::fs;
4use std::num::ParseIntError;
5use std::path::{Path, PathBuf};
6
7use serde::Serialize;
8
9use super::common;
10use crate::common::{WrapIoResult, WrappedIoError};
11
12pub(crate) trait StatsProvider {
13    type Error;
14    type Stats;
15
16    fn stats(cgroup_path: &Path) -> Result<Self::Stats, Self::Error>;
17}
18
19/// Reports the statistics for a cgroup
20#[derive(Debug, Serialize, Default)]
21pub struct Stats {
22    /// Cpu statistics for the cgroup
23    pub cpu: CpuStats,
24    /// Pid statistics for the cgroup
25    pub pids: PidStats,
26    /// Hugetlb statistics for the cgroup
27    pub hugetlb: HashMap<String, HugeTlbStats>,
28    /// Blkio statistics for the cgroup
29    pub blkio: BlkioStats,
30    /// Memory statistics for the cgroup
31    pub memory: MemoryStats,
32}
33
34/// Reports the cpu statistics for a cgroup
35#[derive(Debug, Default, Serialize)]
36pub struct CpuStats {
37    /// Cpu usage statistics for the cgroup
38    pub usage: CpuUsage,
39    /// Cpu Throttling statistics for the cgroup
40    pub throttling: CpuThrottling,
41    /// Pressure Stall Information
42    pub psi: PSIStats,
43}
44
45/// Reports the cpu usage for a cgroup
46#[derive(Debug, Default, PartialEq, Eq, Serialize)]
47pub struct CpuUsage {
48    /// Cpu time consumed by tasks in total
49    pub usage_total: u64,
50    /// Cpu time consumed by tasks in user mode
51    pub usage_user: u64,
52    /// Cpu time consumed by tasks in kernel mode
53    pub usage_kernel: u64,
54    /// Cpu time consumed by tasks itemized per core
55    pub per_core_usage_total: Vec<u64>,
56    /// Cpu time consumed by tasks in user mode itemized per core
57    pub per_core_usage_user: Vec<u64>,
58    /// Cpu time consumed by tasks in kernel mode itemized per core
59    pub per_core_usage_kernel: Vec<u64>,
60}
61
62/// Reports the cpu throttling for a cgroup
63#[derive(Debug, Default, PartialEq, Eq, Serialize)]
64pub struct CpuThrottling {
65    /// Number of period intervals (as specified in cpu.cfs_period_us) that have elapsed
66    pub periods: u64,
67    /// Number of period intervals where tasks have been throttled because they exhausted their quota
68    pub throttled_periods: u64,
69    /// Total time duration for which tasks have been throttled
70    pub throttled_time: u64,
71}
72
73/// Reports memory stats for a cgroup
74#[derive(Debug, Default, Serialize)]
75pub struct MemoryStats {
76    /// Usage of memory
77    pub memory: MemoryData,
78    /// Usage of memory and swap
79    pub memswap: MemoryData,
80    /// Usage of kernel memory
81    pub kernel: MemoryData,
82    /// Usage of kernel tcp memory
83    pub kernel_tcp: MemoryData,
84    /// Page cache in bytes
85    pub cache: u64,
86    /// Returns true if hierarchical accounting is enabled
87    pub hierarchy: bool,
88    /// Various memory statistics
89    pub stats: HashMap<String, u64>,
90    /// Pressure Stall Information
91    pub psi: PSIStats,
92}
93
94/// Reports memory stats for one type of memory
95#[derive(Debug, Default, PartialEq, Eq, Serialize)]
96pub struct MemoryData {
97    /// Usage in bytes
98    pub usage: u64,
99    /// Maximum recorded usage in bytes
100    pub max_usage: u64,
101    /// Number of times memory usage hit limits
102    pub fail_count: u64,
103    /// Memory usage limit
104    pub limit: u64,
105}
106
107/// Reports pid stats for a cgroup
108#[derive(Debug, Default, PartialEq, Eq, Serialize)]
109pub struct PidStats {
110    /// Current number of active pids
111    pub current: u64,
112    /// Allowed number of active pids (0 means no limit)
113    pub limit: u64,
114}
115
116/// Reports block io stats for a cgroup
117#[derive(Debug, Default, PartialEq, Serialize)]
118pub struct BlkioStats {
119    // Number of bytes transferred to/from a device by the cgroup
120    pub service_bytes: Vec<BlkioDeviceStat>,
121    // Number of I/O operations performed on a device by the cgroup
122    pub serviced: Vec<BlkioDeviceStat>,
123    // Time in milliseconds that the cgroup had access to a device
124    pub time: Vec<BlkioDeviceStat>,
125    // Number of sectors transferred to/from a device by the cgroup
126    pub sectors: Vec<BlkioDeviceStat>,
127    // Total time between request dispatch and request completion
128    pub service_time: Vec<BlkioDeviceStat>,
129    // Total time spend waiting in the scheduler queues for service
130    pub wait_time: Vec<BlkioDeviceStat>,
131    // Number of requests queued for I/O operations
132    pub queued: Vec<BlkioDeviceStat>,
133    // Number of requests merged into requests for I/O operations
134    pub merged: Vec<BlkioDeviceStat>,
135    /// Pressure Stall Information
136    pub psi: PSIStats,
137}
138
139/// Reports single stat value for a specific device
140#[derive(Debug, PartialEq, Eq, Clone, Serialize, PartialOrd, Ord)]
141pub struct BlkioDeviceStat {
142    /// Major device number
143    pub major: u64,
144    /// Minor device number
145    pub minor: u64,
146    /// Operation type
147    pub op_type: Option<String>,
148    /// Stat value
149    pub value: u64,
150}
151
152impl Display for BlkioDeviceStat {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        if let Some(op_type) = &self.op_type {
155            write!(
156                f,
157                "{}:{} {} {}",
158                self.major, self.minor, op_type, self.value
159            )
160        } else {
161            write!(f, "{}:{} {}", self.major, self.minor, self.value)
162        }
163    }
164}
165
166/// Reports hugetlb stats for a cgroup
167#[derive(Debug, Default, PartialEq, Eq, Serialize)]
168pub struct HugeTlbStats {
169    /// Current usage in bytes
170    pub usage: u64,
171    /// Maximum recorded usage in bytes
172    pub max_usage: u64,
173    /// Number of allocation failures due to HugeTlb usage limit
174    pub fail_count: u64,
175}
176
177/// Reports Pressure Stall Information for a cgroup
178#[derive(Debug, Default, PartialEq, Serialize)]
179pub struct PSIStats {
180    /// Percentage of walltime that some (one or more) tasks were delayed due to lack of resources
181    pub some: PSIData,
182    /// Percentage of walltime in which all tasks were delayed by lack of resources
183    pub full: PSIData,
184}
185
186#[derive(Debug, Default, PartialEq, Serialize)]
187pub struct PSIData {
188    /// Running average over the last 10 seconds
189    pub avg10: f64,
190    /// Running average over the last 60 seconds
191    pub avg60: f64,
192    /// Running average over the last 300 seconds
193    pub avg300: f64,
194}
195
196#[derive(thiserror::Error, Debug)]
197pub enum SupportedPageSizesError {
198    #[error("io error: {0}")]
199    Io(#[from] std::io::Error),
200    #[error("failed to parse value {value}: {err}")]
201    Parse { value: String, err: ParseIntError },
202    #[error("failed to determine page size from {dir_name}")]
203    Failed { dir_name: String },
204}
205
206/// Reports which hugepage sizes are supported by the system
207pub fn supported_page_sizes() -> Result<Vec<String>, SupportedPageSizesError> {
208    let mut sizes = Vec::new();
209    for hugetlb_entry in fs::read_dir("/sys/kernel/mm/hugepages")? {
210        let hugetlb_entry = hugetlb_entry?;
211        if !hugetlb_entry.path().is_dir() {
212            continue;
213        }
214
215        let dir_name = hugetlb_entry.file_name();
216        // this name should always be valid utf-8,
217        // so can unwrap without any checks
218        let dir_name = dir_name.to_str().unwrap();
219
220        sizes.push(extract_page_size(dir_name)?);
221    }
222
223    Ok(sizes)
224}
225
226fn extract_page_size(dir_name: &str) -> Result<String, SupportedPageSizesError> {
227    if let Some(size) = dir_name
228        .strip_prefix("hugepages-")
229        .and_then(|name_stripped| name_stripped.strip_suffix("kB"))
230    {
231        let size: u64 = size.parse().map_err(|err| SupportedPageSizesError::Parse {
232            value: size.into(),
233            err,
234        })?;
235
236        let size_moniker = if size >= (1 << 20) {
237            (size >> 20).to_string() + "GB"
238        } else if size >= (1 << 10) {
239            (size >> 10).to_string() + "MB"
240        } else {
241            size.to_string() + "KB"
242        };
243
244        return Ok(size_moniker);
245    }
246
247    Err(SupportedPageSizesError::Failed {
248        dir_name: dir_name.into(),
249    })
250}
251
252/// Parses this string slice into an u64
253/// # Example
254/// ```
255/// use libcgroups::stats::parse_value;
256///
257/// let value = parse_value("32").unwrap();
258/// assert_eq!(value, 32);
259/// ```
260pub fn parse_value(value: &str) -> Result<u64, ParseIntError> {
261    value.parse()
262}
263
264/// Parses a single valued file to an u64
265/// # Example
266/// ```no_run
267/// use std::path::Path;
268/// use libcgroups::stats::parse_single_value;
269///
270/// let value = parse_single_value(&Path::new("memory.current")).unwrap();
271/// assert_eq!(value, 32);
272/// ```
273pub fn parse_single_value(file_path: &Path) -> Result<u64, WrappedIoError> {
274    let value = common::read_cgroup_file(file_path)?;
275    let value = value.trim();
276    if value == "max" {
277        return Ok(u64::MAX);
278    }
279
280    value
281        .parse()
282        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
283        .wrap_other(file_path)
284}
285
286#[derive(thiserror::Error, Debug)]
287pub enum ParseFlatKeyedDataError {
288    #[error("io error: {0}")]
289    WrappedIo(#[from] WrappedIoError),
290    #[error("flat keyed data at {path} contains entries that do not conform to 'key value'")]
291    DoesNotConform { path: PathBuf },
292    #[error("failed to parse value {value} from {path}")]
293    FailedToParse {
294        value: String,
295        path: PathBuf,
296        err: ParseIntError,
297    },
298}
299
300/// Parses a file that is structured according to the flat keyed format
301pub(crate) fn parse_flat_keyed_data(
302    file_path: &Path,
303) -> Result<HashMap<String, u64>, ParseFlatKeyedDataError> {
304    let mut stats = HashMap::new();
305    let keyed_data = common::read_cgroup_file(file_path)?;
306    for entry in keyed_data.lines() {
307        let entry_fields: Vec<&str> = entry.split_ascii_whitespace().collect();
308        if entry_fields.len() != 2 {
309            return Err(ParseFlatKeyedDataError::DoesNotConform {
310                path: file_path.to_path_buf(),
311            });
312        }
313
314        stats.insert(
315            entry_fields[0].to_owned(),
316            entry_fields[1]
317                .parse()
318                .map_err(|err| ParseFlatKeyedDataError::FailedToParse {
319                    value: entry_fields[0].into(),
320                    path: file_path.to_path_buf(),
321                    err,
322                })?,
323        );
324    }
325
326    Ok(stats)
327}
328
329#[derive(thiserror::Error, Debug)]
330pub enum ParseNestedKeyedDataError {
331    #[error("io error: {0}")]
332    WrappedIo(#[from] WrappedIoError),
333    #[error("nested keyed data at {path} contains entries that do not conform to key format")]
334    DoesNotConform { path: PathBuf },
335}
336
337/// Parses a file that is structured according to the nested keyed format
338pub fn parse_nested_keyed_data(
339    file_path: &Path,
340) -> Result<HashMap<String, Vec<String>>, ParseNestedKeyedDataError> {
341    let mut stats: HashMap<String, Vec<String>> = HashMap::new();
342    let keyed_data = common::read_cgroup_file(file_path)?;
343    for entry in keyed_data.lines() {
344        let entry_fields: Vec<&str> = entry.split_ascii_whitespace().collect();
345        if entry_fields.len() < 2 || !entry_fields[1..].iter().all(|p| p.contains('=')) {
346            return Err(ParseNestedKeyedDataError::DoesNotConform {
347                path: file_path.to_path_buf(),
348            });
349        }
350
351        stats.insert(
352            entry_fields[0].to_owned(),
353            entry_fields[1..]
354                .iter()
355                .copied()
356                .map(|p| p.to_owned())
357                .collect(),
358        );
359    }
360
361    Ok(stats)
362}
363
364#[derive(thiserror::Error, Debug)]
365pub enum ParseDeviceNumberError {
366    #[error("failed to parse device number from {device}: expected 2 parts, found {numbers}")]
367    TooManyNumbers { device: String, numbers: usize },
368    #[error("failed to parse device number from {device}: {err}")]
369    MalformedNumber { device: String, err: ParseIntError },
370}
371
372pub(crate) fn parse_device_number(device: &str) -> Result<(u64, u64), ParseDeviceNumberError> {
373    let numbers: Vec<&str> = device.split_terminator(':').collect();
374    if numbers.len() != 2 {
375        return Err(ParseDeviceNumberError::TooManyNumbers {
376            device: device.into(),
377            numbers: numbers.len(),
378        });
379    }
380
381    Ok((
382        numbers[0]
383            .parse()
384            .map_err(|err| ParseDeviceNumberError::MalformedNumber {
385                device: device.into(),
386                err,
387            })?,
388        numbers[1]
389            .parse()
390            .map_err(|err| ParseDeviceNumberError::MalformedNumber {
391                device: device.into(),
392                err,
393            })?,
394    ))
395}
396
397#[derive(thiserror::Error, Debug)]
398pub enum PidStatsError {
399    #[error("io error: {0}")]
400    WrappedIo(#[from] WrappedIoError),
401    #[error("failed to parse current pids: {0}")]
402    ParseCurrent(ParseIntError),
403    #[error("failed to parse pids limit: {0}")]
404    ParseLimit(ParseIntError),
405}
406
407/// Returns cgroup pid statistics
408pub fn pid_stats(cgroup_path: &Path) -> Result<PidStats, PidStatsError> {
409    let mut stats = PidStats::default();
410
411    let current = common::read_cgroup_file(cgroup_path.join("pids.current"))?;
412    stats.current = current
413        .trim()
414        .parse()
415        .map_err(PidStatsError::ParseCurrent)?;
416
417    let limit =
418        common::read_cgroup_file(cgroup_path.join("pids.max")).map(|l| l.trim().to_owned())?;
419    if limit != "max" {
420        stats.limit = limit.parse().map_err(PidStatsError::ParseLimit)?;
421    }
422
423    Ok(stats)
424}
425
426pub fn psi_stats(psi_file: &Path) -> Result<PSIStats, WrappedIoError> {
427    let mut stats = PSIStats::default();
428
429    let psi = common::read_cgroup_file(psi_file)?;
430    for line in psi.lines() {
431        match &line[0..4] {
432            "some" => stats.some = parse_psi(&line[4..], psi_file)?,
433            "full" => stats.full = parse_psi(&line[4..], psi_file)?,
434            _ => continue,
435        }
436    }
437
438    Ok(stats)
439}
440
441fn parse_psi(stat_line: &str, path: &Path) -> Result<PSIData, WrappedIoError> {
442    use std::io::{Error, ErrorKind};
443
444    let mut psi_data = PSIData::default();
445
446    for kv in stat_line.split_ascii_whitespace() {
447        match kv.split_once('=') {
448            Some(("avg10", v)) => {
449                psi_data.avg10 = v
450                    .parse()
451                    .map_err(|err| Error::new(ErrorKind::InvalidData, err))
452                    .wrap_other(path)?
453            }
454            Some(("avg60", v)) => {
455                psi_data.avg60 = v
456                    .parse()
457                    .map_err(|err| Error::new(ErrorKind::InvalidData, err))
458                    .wrap_other(path)?
459            }
460            Some(("avg300", v)) => {
461                psi_data.avg300 = v
462                    .parse()
463                    .map_err(|err| Error::new(ErrorKind::InvalidData, err))
464                    .wrap_other(path)?
465            }
466            _ => continue,
467        }
468    }
469
470    Ok(psi_data)
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::test::set_fixture;
477
478    #[test]
479    fn test_supported_page_sizes_gigabyte() {
480        let page_size = extract_page_size("hugepages-1048576kB").unwrap();
481        assert_eq!(page_size, "1GB");
482    }
483
484    #[test]
485    fn test_supported_page_sizes_megabyte() {
486        let page_size = extract_page_size("hugepages-2048kB").unwrap();
487        assert_eq!(page_size, "2MB");
488    }
489
490    #[test]
491    fn test_supported_page_sizes_kilobyte() {
492        let page_size = extract_page_size("hugepages-512kB").unwrap();
493        assert_eq!(page_size, "512KB");
494    }
495
496    #[test]
497    fn test_parse_single_value_valid() {
498        let tmp = tempfile::tempdir().unwrap();
499        let file_path = set_fixture(tmp.path(), "single_valued_file", "1200\n").unwrap();
500
501        let value = parse_single_value(&file_path).unwrap();
502        assert_eq!(value, 1200);
503    }
504
505    #[test]
506    fn test_parse_single_value_invalid_number() {
507        let tmp = tempfile::tempdir().unwrap();
508        let file_path = set_fixture(tmp.path(), "single_invalid_file", "noop\n").unwrap();
509
510        let value = parse_single_value(&file_path);
511        assert!(value.is_err());
512    }
513
514    #[test]
515    fn test_parse_single_value_multiple_entries() {
516        let tmp = tempfile::tempdir().unwrap();
517        let file_path = set_fixture(tmp.path(), "multi_valued_file", "1200\n1400\n1600").unwrap();
518
519        let value = parse_single_value(&file_path);
520        assert!(value.is_err());
521    }
522
523    #[test]
524    fn test_parse_flat_keyed_data() {
525        let tmp = tempfile::tempdir().unwrap();
526        let file_content = ["key1 1", "key2 2", "key3 3"].join("\n");
527        let file_path = set_fixture(tmp.path(), "flat_keyed_data", &file_content).unwrap();
528
529        let actual = parse_flat_keyed_data(&file_path).unwrap();
530        let mut expected = HashMap::with_capacity(3);
531        expected.insert("key1".to_owned(), 1);
532        expected.insert("key2".to_owned(), 2);
533        expected.insert("key3".to_owned(), 3);
534
535        assert_eq!(actual, expected);
536    }
537
538    #[test]
539    fn test_parse_flat_keyed_data_with_characters() {
540        let tmp = tempfile::tempdir().unwrap();
541        let file_content = ["key1 1", "key2 a", "key3 b"].join("\n");
542        let file_path = set_fixture(tmp.path(), "flat_keyed_data", &file_content).unwrap();
543
544        let result = parse_flat_keyed_data(&file_path);
545        assert!(result.is_err());
546    }
547
548    #[test]
549    fn test_parse_space_separated_as_flat_keyed_data() {
550        let tmp = tempfile::tempdir().unwrap();
551        let file_content = ["key1", "key2", "key3", "key4"].join(" ");
552        let file_path = set_fixture(tmp.path(), "space_separated", &file_content).unwrap();
553
554        let result = parse_flat_keyed_data(&file_path);
555        assert!(result.is_err());
556    }
557
558    #[test]
559    fn test_parse_newline_separated_as_flat_keyed_data() {
560        let tmp = tempfile::tempdir().unwrap();
561        let file_content = ["key1", "key2", "key3", "key4"].join("\n");
562        let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
563
564        let result = parse_flat_keyed_data(&file_path);
565        assert!(result.is_err());
566    }
567
568    #[test]
569    fn test_parse_nested_keyed_data_as_flat_keyed_data() {
570        let tmp = tempfile::tempdir().unwrap();
571        let file_content = [
572            "key1 subkey1=value1 subkey2=value2 subkey3=value3",
573            "key2 subkey1=value1 subkey2=value2 subkey3=value3",
574            "key3 subkey1=value1 subkey2=value2 subkey3=value3",
575        ]
576        .join("\n");
577        let file_path = set_fixture(tmp.path(), "nested_keyed_data", &file_content).unwrap();
578
579        let result = parse_flat_keyed_data(&file_path);
580        assert!(result.is_err());
581    }
582
583    #[test]
584    fn test_parse_nested_keyed_data() {
585        let tmp = tempfile::tempdir().unwrap();
586        let file_content = [
587            "key1 subkey1=value1 subkey2=value2 subkey3=value3",
588            "key2 subkey1=value1 subkey2=value2 subkey3=value3",
589            "key3 subkey1=value1 subkey2=value2 subkey3=value3",
590        ]
591        .join("\n");
592        let file_path = set_fixture(tmp.path(), "nested_keyed_data", &file_content).unwrap();
593
594        let actual = parse_nested_keyed_data(&file_path).unwrap();
595        let mut expected = HashMap::with_capacity(3);
596        expected.insert(
597            "key1".to_owned(),
598            vec![
599                "subkey1=value1".to_owned(),
600                "subkey2=value2".to_owned(),
601                "subkey3=value3".to_owned(),
602            ],
603        );
604        expected.insert(
605            "key2".to_owned(),
606            vec![
607                "subkey1=value1".to_owned(),
608                "subkey2=value2".to_owned(),
609                "subkey3=value3".to_owned(),
610            ],
611        );
612        expected.insert(
613            "key3".to_owned(),
614            vec![
615                "subkey1=value1".to_owned(),
616                "subkey2=value2".to_owned(),
617                "subkey3=value3".to_owned(),
618            ],
619        );
620
621        assert_eq!(actual, expected);
622    }
623
624    #[test]
625    fn test_parse_space_separated_as_nested_keyed_data() {
626        let tmp = tempfile::tempdir().unwrap();
627        let file_content = ["key1", "key2", "key3", "key4"].join(" ");
628        let file_path = set_fixture(tmp.path(), "space_separated", &file_content).unwrap();
629
630        let result = parse_nested_keyed_data(&file_path);
631        assert!(result.is_err());
632    }
633
634    #[test]
635    fn test_parse_newline_separated_as_nested_keyed_data() {
636        let tmp = tempfile::tempdir().unwrap();
637        let file_content = ["key1", "key2", "key3", "key4"].join("\n");
638        let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
639
640        let result = parse_nested_keyed_data(&file_path);
641        assert!(result.is_err());
642    }
643
644    #[test]
645    fn test_parse_flat_keyed_as_nested_keyed_data() {
646        let tmp = tempfile::tempdir().unwrap();
647        let file_content = ["key1 1", "key2 2", "key3 3"].join("\n");
648        let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
649
650        let result = parse_nested_keyed_data(&file_path);
651        assert!(result.is_err());
652    }
653
654    #[test]
655    fn test_parse_device_number() {
656        let (major, minor) = parse_device_number("8:0").unwrap();
657        assert_eq!((major, minor), (8, 0));
658    }
659
660    #[test]
661    fn test_parse_invalid_device_number() {
662        let result = parse_device_number("a:b");
663        assert!(result.is_err());
664    }
665
666    #[test]
667    fn test_parse_psi_full_stats() {
668        let tmp = tempfile::tempdir().unwrap();
669        let file_content = [
670            "some avg10=80.00 avg60=50.00 avg300=90.00 total=0",
671            "full avg10=10.00 avg60=30.00 avg300=50.00 total=0",
672        ]
673        .join("\n");
674        let psi_file = set_fixture(tmp.path(), "psi.pressure", &file_content).unwrap();
675
676        let result = psi_stats(&psi_file).unwrap();
677        assert_eq!(
678            result,
679            PSIStats {
680                some: PSIData {
681                    avg10: 80.0,
682                    avg60: 50.0,
683                    avg300: 90.0
684                },
685                full: PSIData {
686                    avg10: 10.0,
687                    avg60: 30.0,
688                    avg300: 50.0
689                },
690            }
691        )
692    }
693
694    #[test]
695    fn test_parse_psi_only_some() {
696        let tmp = tempfile::tempdir().unwrap();
697        let file_content = ["some avg10=80.00 avg60=50.00 avg300=90.00 total=0"].join("\n");
698        let psi_file = set_fixture(tmp.path(), "psi.pressure", &file_content).unwrap();
699
700        let result = psi_stats(&psi_file).unwrap();
701        assert_eq!(
702            result,
703            PSIStats {
704                some: PSIData {
705                    avg10: 80.0,
706                    avg60: 50.0,
707                    avg300: 90.0
708                },
709                full: PSIData::default(),
710            }
711        )
712    }
713}