use std::collections::HashMap;
use std::fmt::Display;
use std::fs;
use std::num::ParseIntError;
use std::path::{Path, PathBuf};
use serde::Serialize;
use super::common;
use crate::common::{WrapIoResult, WrappedIoError};
pub(crate) trait StatsProvider {
type Error;
type Stats;
fn stats(cgroup_path: &Path) -> Result<Self::Stats, Self::Error>;
}
#[derive(Debug, Serialize, Default)]
pub struct Stats {
pub cpu: CpuStats,
pub pids: PidStats,
pub hugetlb: HashMap<String, HugeTlbStats>,
pub blkio: BlkioStats,
pub memory: MemoryStats,
}
#[derive(Debug, Default, Serialize)]
pub struct CpuStats {
pub usage: CpuUsage,
pub throttling: CpuThrottling,
pub psi: PSIStats,
}
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct CpuUsage {
pub usage_total: u64,
pub usage_user: u64,
pub usage_kernel: u64,
pub per_core_usage_total: Vec<u64>,
pub per_core_usage_user: Vec<u64>,
pub per_core_usage_kernel: Vec<u64>,
}
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct CpuThrottling {
pub periods: u64,
pub throttled_periods: u64,
pub throttled_time: u64,
}
#[derive(Debug, Default, Serialize)]
pub struct MemoryStats {
pub memory: MemoryData,
pub memswap: MemoryData,
pub kernel: MemoryData,
pub kernel_tcp: MemoryData,
pub cache: u64,
pub hierarchy: bool,
pub stats: HashMap<String, u64>,
pub psi: PSIStats,
}
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct MemoryData {
pub usage: u64,
pub max_usage: u64,
pub fail_count: u64,
pub limit: u64,
}
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct PidStats {
pub current: u64,
pub limit: u64,
}
#[derive(Debug, Default, PartialEq, Serialize)]
pub struct BlkioStats {
pub service_bytes: Vec<BlkioDeviceStat>,
pub serviced: Vec<BlkioDeviceStat>,
pub time: Vec<BlkioDeviceStat>,
pub sectors: Vec<BlkioDeviceStat>,
pub service_time: Vec<BlkioDeviceStat>,
pub wait_time: Vec<BlkioDeviceStat>,
pub queued: Vec<BlkioDeviceStat>,
pub merged: Vec<BlkioDeviceStat>,
pub psi: PSIStats,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, PartialOrd, Ord)]
pub struct BlkioDeviceStat {
pub major: u64,
pub minor: u64,
pub op_type: Option<String>,
pub value: u64,
}
impl Display for BlkioDeviceStat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(op_type) = &self.op_type {
write!(
f,
"{}:{} {} {}",
self.major, self.minor, op_type, self.value
)
} else {
write!(f, "{}:{} {}", self.major, self.minor, self.value)
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct HugeTlbStats {
pub usage: u64,
pub max_usage: u64,
pub fail_count: u64,
}
#[derive(Debug, Default, PartialEq, Serialize)]
pub struct PSIStats {
pub some: PSIData,
pub full: PSIData,
}
#[derive(Debug, Default, PartialEq, Serialize)]
pub struct PSIData {
pub avg10: f64,
pub avg60: f64,
pub avg300: f64,
}
#[derive(thiserror::Error, Debug)]
pub enum SupportedPageSizesError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("failed to parse value {value}: {err}")]
Parse { value: String, err: ParseIntError },
#[error("failed to determine page size from {dir_name}")]
Failed { dir_name: String },
}
pub fn supported_page_sizes() -> Result<Vec<String>, SupportedPageSizesError> {
let mut sizes = Vec::new();
for hugetlb_entry in fs::read_dir("/sys/kernel/mm/hugepages")? {
let hugetlb_entry = hugetlb_entry?;
if !hugetlb_entry.path().is_dir() {
continue;
}
let dir_name = hugetlb_entry.file_name();
let dir_name = dir_name.to_str().unwrap();
sizes.push(extract_page_size(dir_name)?);
}
Ok(sizes)
}
fn extract_page_size(dir_name: &str) -> Result<String, SupportedPageSizesError> {
if let Some(size) = dir_name
.strip_prefix("hugepages-")
.and_then(|name_stripped| name_stripped.strip_suffix("kB"))
{
let size: u64 = size.parse().map_err(|err| SupportedPageSizesError::Parse {
value: size.into(),
err,
})?;
let size_moniker = if size >= (1 << 20) {
(size >> 20).to_string() + "GB"
} else if size >= (1 << 10) {
(size >> 10).to_string() + "MB"
} else {
size.to_string() + "KB"
};
return Ok(size_moniker);
}
Err(SupportedPageSizesError::Failed {
dir_name: dir_name.into(),
})
}
pub fn parse_value(value: &str) -> Result<u64, ParseIntError> {
value.parse()
}
pub fn parse_single_value(file_path: &Path) -> Result<u64, WrappedIoError> {
let value = common::read_cgroup_file(file_path)?;
let value = value.trim();
if value == "max" {
return Ok(u64::MAX);
}
value
.parse()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
.wrap_other(file_path)
}
#[derive(thiserror::Error, Debug)]
pub enum ParseFlatKeyedDataError {
#[error("io error: {0}")]
WrappedIo(#[from] WrappedIoError),
#[error("flat keyed data at {path} contains entries that do not conform to 'key value'")]
DoesNotConform { path: PathBuf },
#[error("failed to parse value {value} from {path}")]
FailedToParse {
value: String,
path: PathBuf,
err: ParseIntError,
},
}
pub(crate) fn parse_flat_keyed_data(
file_path: &Path,
) -> Result<HashMap<String, u64>, ParseFlatKeyedDataError> {
let mut stats = HashMap::new();
let keyed_data = common::read_cgroup_file(file_path)?;
for entry in keyed_data.lines() {
let entry_fields: Vec<&str> = entry.split_ascii_whitespace().collect();
if entry_fields.len() != 2 {
return Err(ParseFlatKeyedDataError::DoesNotConform {
path: file_path.to_path_buf(),
});
}
stats.insert(
entry_fields[0].to_owned(),
entry_fields[1]
.parse()
.map_err(|err| ParseFlatKeyedDataError::FailedToParse {
value: entry_fields[0].into(),
path: file_path.to_path_buf(),
err,
})?,
);
}
Ok(stats)
}
#[derive(thiserror::Error, Debug)]
pub enum ParseNestedKeyedDataError {
#[error("io error: {0}")]
WrappedIo(#[from] WrappedIoError),
#[error("nested keyed data at {path} contains entries that do not conform to key format")]
DoesNotConform { path: PathBuf },
}
pub fn parse_nested_keyed_data(
file_path: &Path,
) -> Result<HashMap<String, Vec<String>>, ParseNestedKeyedDataError> {
let mut stats: HashMap<String, Vec<String>> = HashMap::new();
let keyed_data = common::read_cgroup_file(file_path)?;
for entry in keyed_data.lines() {
let entry_fields: Vec<&str> = entry.split_ascii_whitespace().collect();
if entry_fields.len() < 2 || !entry_fields[1..].iter().all(|p| p.contains('=')) {
return Err(ParseNestedKeyedDataError::DoesNotConform {
path: file_path.to_path_buf(),
});
}
stats.insert(
entry_fields[0].to_owned(),
entry_fields[1..]
.iter()
.copied()
.map(|p| p.to_owned())
.collect(),
);
}
Ok(stats)
}
#[derive(thiserror::Error, Debug)]
pub enum ParseDeviceNumberError {
#[error("failed to parse device number from {device}: expected 2 parts, found {numbers}")]
TooManyNumbers { device: String, numbers: usize },
#[error("failed to parse device number from {device}: {err}")]
MalformedNumber { device: String, err: ParseIntError },
}
pub(crate) fn parse_device_number(device: &str) -> Result<(u64, u64), ParseDeviceNumberError> {
let numbers: Vec<&str> = device.split_terminator(':').collect();
if numbers.len() != 2 {
return Err(ParseDeviceNumberError::TooManyNumbers {
device: device.into(),
numbers: numbers.len(),
});
}
Ok((
numbers[0]
.parse()
.map_err(|err| ParseDeviceNumberError::MalformedNumber {
device: device.into(),
err,
})?,
numbers[1]
.parse()
.map_err(|err| ParseDeviceNumberError::MalformedNumber {
device: device.into(),
err,
})?,
))
}
#[derive(thiserror::Error, Debug)]
pub enum PidStatsError {
#[error("io error: {0}")]
WrappedIo(#[from] WrappedIoError),
#[error("failed to parse current pids: {0}")]
ParseCurrent(ParseIntError),
#[error("failed to parse pids limit: {0}")]
ParseLimit(ParseIntError),
}
pub fn pid_stats(cgroup_path: &Path) -> Result<PidStats, PidStatsError> {
let mut stats = PidStats::default();
let current = common::read_cgroup_file(cgroup_path.join("pids.current"))?;
stats.current = current
.trim()
.parse()
.map_err(PidStatsError::ParseCurrent)?;
let limit =
common::read_cgroup_file(cgroup_path.join("pids.max")).map(|l| l.trim().to_owned())?;
if limit != "max" {
stats.limit = limit.parse().map_err(PidStatsError::ParseLimit)?;
}
Ok(stats)
}
pub fn psi_stats(psi_file: &Path) -> Result<PSIStats, WrappedIoError> {
let mut stats = PSIStats::default();
let psi = common::read_cgroup_file(psi_file)?;
for line in psi.lines() {
match &line[0..4] {
"some" => stats.some = parse_psi(&line[4..], psi_file)?,
"full" => stats.full = parse_psi(&line[4..], psi_file)?,
_ => continue,
}
}
Ok(stats)
}
fn parse_psi(stat_line: &str, path: &Path) -> Result<PSIData, WrappedIoError> {
use std::io::{Error, ErrorKind};
let mut psi_data = PSIData::default();
for kv in stat_line.split_ascii_whitespace() {
match kv.split_once('=') {
Some(("avg10", v)) => {
psi_data.avg10 = v
.parse()
.map_err(|err| Error::new(ErrorKind::InvalidData, err))
.wrap_other(path)?
}
Some(("avg60", v)) => {
psi_data.avg60 = v
.parse()
.map_err(|err| Error::new(ErrorKind::InvalidData, err))
.wrap_other(path)?
}
Some(("avg300", v)) => {
psi_data.avg300 = v
.parse()
.map_err(|err| Error::new(ErrorKind::InvalidData, err))
.wrap_other(path)?
}
_ => continue,
}
}
Ok(psi_data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::set_fixture;
#[test]
fn test_supported_page_sizes_gigabyte() {
let page_size = extract_page_size("hugepages-1048576kB").unwrap();
assert_eq!(page_size, "1GB");
}
#[test]
fn test_supported_page_sizes_megabyte() {
let page_size = extract_page_size("hugepages-2048kB").unwrap();
assert_eq!(page_size, "2MB");
}
#[test]
fn test_supported_page_sizes_kilobyte() {
let page_size = extract_page_size("hugepages-512kB").unwrap();
assert_eq!(page_size, "512KB");
}
#[test]
fn test_parse_single_value_valid() {
let tmp = tempfile::tempdir().unwrap();
let file_path = set_fixture(tmp.path(), "single_valued_file", "1200\n").unwrap();
let value = parse_single_value(&file_path).unwrap();
assert_eq!(value, 1200);
}
#[test]
fn test_parse_single_value_invalid_number() {
let tmp = tempfile::tempdir().unwrap();
let file_path = set_fixture(tmp.path(), "single_invalid_file", "noop\n").unwrap();
let value = parse_single_value(&file_path);
assert!(value.is_err());
}
#[test]
fn test_parse_single_value_multiple_entries() {
let tmp = tempfile::tempdir().unwrap();
let file_path = set_fixture(tmp.path(), "multi_valued_file", "1200\n1400\n1600").unwrap();
let value = parse_single_value(&file_path);
assert!(value.is_err());
}
#[test]
fn test_parse_flat_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1 1", "key2 2", "key3 3"].join("\n");
let file_path = set_fixture(tmp.path(), "flat_keyed_data", &file_content).unwrap();
let actual = parse_flat_keyed_data(&file_path).unwrap();
let mut expected = HashMap::with_capacity(3);
expected.insert("key1".to_owned(), 1);
expected.insert("key2".to_owned(), 2);
expected.insert("key3".to_owned(), 3);
assert_eq!(actual, expected);
}
#[test]
fn test_parse_flat_keyed_data_with_characters() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1 1", "key2 a", "key3 b"].join("\n");
let file_path = set_fixture(tmp.path(), "flat_keyed_data", &file_content).unwrap();
let result = parse_flat_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_space_separated_as_flat_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1", "key2", "key3", "key4"].join(" ");
let file_path = set_fixture(tmp.path(), "space_separated", &file_content).unwrap();
let result = parse_flat_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_newline_separated_as_flat_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1", "key2", "key3", "key4"].join("\n");
let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
let result = parse_flat_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_nested_keyed_data_as_flat_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = [
"key1 subkey1=value1 subkey2=value2 subkey3=value3",
"key2 subkey1=value1 subkey2=value2 subkey3=value3",
"key3 subkey1=value1 subkey2=value2 subkey3=value3",
]
.join("\n");
let file_path = set_fixture(tmp.path(), "nested_keyed_data", &file_content).unwrap();
let result = parse_flat_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_nested_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = [
"key1 subkey1=value1 subkey2=value2 subkey3=value3",
"key2 subkey1=value1 subkey2=value2 subkey3=value3",
"key3 subkey1=value1 subkey2=value2 subkey3=value3",
]
.join("\n");
let file_path = set_fixture(tmp.path(), "nested_keyed_data", &file_content).unwrap();
let actual = parse_nested_keyed_data(&file_path).unwrap();
let mut expected = HashMap::with_capacity(3);
expected.insert(
"key1".to_owned(),
vec![
"subkey1=value1".to_owned(),
"subkey2=value2".to_owned(),
"subkey3=value3".to_owned(),
],
);
expected.insert(
"key2".to_owned(),
vec![
"subkey1=value1".to_owned(),
"subkey2=value2".to_owned(),
"subkey3=value3".to_owned(),
],
);
expected.insert(
"key3".to_owned(),
vec![
"subkey1=value1".to_owned(),
"subkey2=value2".to_owned(),
"subkey3=value3".to_owned(),
],
);
assert_eq!(actual, expected);
}
#[test]
fn test_parse_space_separated_as_nested_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1", "key2", "key3", "key4"].join(" ");
let file_path = set_fixture(tmp.path(), "space_separated", &file_content).unwrap();
let result = parse_nested_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_newline_separated_as_nested_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1", "key2", "key3", "key4"].join("\n");
let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
let result = parse_nested_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_flat_keyed_as_nested_keyed_data() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["key1 1", "key2 2", "key3 3"].join("\n");
let file_path = set_fixture(tmp.path(), "newline_separated", &file_content).unwrap();
let result = parse_nested_keyed_data(&file_path);
assert!(result.is_err());
}
#[test]
fn test_parse_device_number() {
let (major, minor) = parse_device_number("8:0").unwrap();
assert_eq!((major, minor), (8, 0));
}
#[test]
fn test_parse_invalid_device_number() {
let result = parse_device_number("a:b");
assert!(result.is_err());
}
#[test]
fn test_parse_psi_full_stats() {
let tmp = tempfile::tempdir().unwrap();
let file_content = [
"some avg10=80.00 avg60=50.00 avg300=90.00 total=0",
"full avg10=10.00 avg60=30.00 avg300=50.00 total=0",
]
.join("\n");
let psi_file = set_fixture(tmp.path(), "psi.pressure", &file_content).unwrap();
let result = psi_stats(&psi_file).unwrap();
assert_eq!(
result,
PSIStats {
some: PSIData {
avg10: 80.0,
avg60: 50.0,
avg300: 90.0
},
full: PSIData {
avg10: 10.0,
avg60: 30.0,
avg300: 50.0
},
}
)
}
#[test]
fn test_parse_psi_only_some() {
let tmp = tempfile::tempdir().unwrap();
let file_content = ["some avg10=80.00 avg60=50.00 avg300=90.00 total=0"].join("\n");
let psi_file = set_fixture(tmp.path(), "psi.pressure", &file_content).unwrap();
let result = psi_stats(&psi_file).unwrap();
assert_eq!(
result,
PSIStats {
some: PSIData {
avg10: 80.0,
avg60: 50.0,
avg300: 90.0
},
full: PSIData::default(),
}
)
}
}