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#[derive(Debug, Serialize, Default)]
21pub struct Stats {
22 pub cpu: CpuStats,
24 pub pids: PidStats,
26 pub hugetlb: HashMap<String, HugeTlbStats>,
28 pub blkio: BlkioStats,
30 pub memory: MemoryStats,
32}
33
34#[derive(Debug, Default, Serialize)]
36pub struct CpuStats {
37 pub usage: CpuUsage,
39 pub throttling: CpuThrottling,
41 pub psi: PSIStats,
43}
44
45#[derive(Debug, Default, PartialEq, Eq, Serialize)]
47pub struct CpuUsage {
48 pub usage_total: u64,
50 pub usage_user: u64,
52 pub usage_kernel: u64,
54 pub per_core_usage_total: Vec<u64>,
56 pub per_core_usage_user: Vec<u64>,
58 pub per_core_usage_kernel: Vec<u64>,
60}
61
62#[derive(Debug, Default, PartialEq, Eq, Serialize)]
64pub struct CpuThrottling {
65 pub periods: u64,
67 pub throttled_periods: u64,
69 pub throttled_time: u64,
71}
72
73#[derive(Debug, Default, Serialize)]
75pub struct MemoryStats {
76 pub memory: MemoryData,
78 pub memswap: MemoryData,
80 pub kernel: MemoryData,
82 pub kernel_tcp: MemoryData,
84 pub cache: u64,
86 pub hierarchy: bool,
88 pub stats: HashMap<String, u64>,
90 pub psi: PSIStats,
92}
93
94#[derive(Debug, Default, PartialEq, Eq, Serialize)]
96pub struct MemoryData {
97 pub usage: u64,
99 pub max_usage: u64,
101 pub fail_count: u64,
103 pub limit: u64,
105}
106
107#[derive(Debug, Default, PartialEq, Eq, Serialize)]
109pub struct PidStats {
110 pub current: u64,
112 pub limit: u64,
114}
115
116#[derive(Debug, Default, PartialEq, Serialize)]
118pub struct BlkioStats {
119 pub service_bytes: Vec<BlkioDeviceStat>,
121 pub serviced: Vec<BlkioDeviceStat>,
123 pub time: Vec<BlkioDeviceStat>,
125 pub sectors: Vec<BlkioDeviceStat>,
127 pub service_time: Vec<BlkioDeviceStat>,
129 pub wait_time: Vec<BlkioDeviceStat>,
131 pub queued: Vec<BlkioDeviceStat>,
133 pub merged: Vec<BlkioDeviceStat>,
135 pub psi: PSIStats,
137}
138
139#[derive(Debug, PartialEq, Eq, Clone, Serialize, PartialOrd, Ord)]
141pub struct BlkioDeviceStat {
142 pub major: u64,
144 pub minor: u64,
146 pub op_type: Option<String>,
148 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#[derive(Debug, Default, PartialEq, Eq, Serialize)]
168pub struct HugeTlbStats {
169 pub usage: u64,
171 pub max_usage: u64,
173 pub fail_count: u64,
175}
176
177#[derive(Debug, Default, PartialEq, Serialize)]
179pub struct PSIStats {
180 pub some: PSIData,
182 pub full: PSIData,
184}
185
186#[derive(Debug, Default, PartialEq, Serialize)]
187pub struct PSIData {
188 pub avg10: f64,
190 pub avg60: f64,
192 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
206pub 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 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
252pub fn parse_value(value: &str) -> Result<u64, ParseIntError> {
261 value.parse()
262}
263
264pub 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
300pub(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
337pub 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
407pub 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}