diff options
author | Brian Chen <brianc118@meta.com> | 2023-12-06 10:17:58 -0800 |
---|---|---|
committer | Facebook GitHub Bot <facebook-github-bot@users.noreply.github.com> | 2023-12-06 10:17:58 -0800 |
commit | 5151c5a2dc07e3fcf9b3bf2a4416c67a631c1c08 (patch) | |
tree | 0b206381306a17acc14e56ad1dbd57d364854275 | |
parent | ba700bd2d01bcbc160ce6848e481d37d1940a63b (diff) |
Add resctrl crate for reading /sys/fs/resctrl
Summary:
Add crate for reading /sys/fs/resctrl.
https://www.kernel.org/doc/html/v6.4/arch/x86/resctrl.html
Reviewed By: dschatzberg
Differential Revision: D51438295
fbshipit-source-id: 83b1b147b3a61e36fee2b581bf8634a203d5300c
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | below/resctrlfs/Cargo.toml | 22 | ||||
-rw-r--r-- | below/resctrlfs/src/lib.rs | 414 | ||||
-rw-r--r-- | below/resctrlfs/src/test.rs | 537 | ||||
-rw-r--r-- | below/resctrlfs/src/types.rs | 89 |
5 files changed, 1063 insertions, 0 deletions
@@ -12,6 +12,7 @@ members = [ "below/model", "below/procfs", "below/render", + "below/resctrlfs", "below/store", "below/view", ] diff --git a/below/resctrlfs/Cargo.toml b/below/resctrlfs/Cargo.toml new file mode 100644 index 00000000..b8449076 --- /dev/null +++ b/below/resctrlfs/Cargo.toml @@ -0,0 +1,22 @@ +# @generated by autocargo from //resctl/below/resctrlfs:resctrlfs + +[package] +name = "resctrlfs" +version = "0.7.1" +authors = ["Daniel Xu <dlxu@fb.com>", "Facebook"] +edition = "2021" +description = "A crate for reading resctrl fs data" +readme = "README" +repository = "https://github.com/facebookincubator/below" +license = "Apache-2.0" + +[dependencies] +nix = "0.25" +openat = "0.1.21" +serde = { version = "1.0.185", features = ["derive", "rc"] } +thiserror = "1.0.49" + +[dev-dependencies] +maplit = "1.0" +paste = "1.0.14" +tempfile = "3.8" diff --git a/below/resctrlfs/src/lib.rs b/below/resctrlfs/src/lib.rs new file mode 100644 index 00000000..ddec933d --- /dev/null +++ b/below/resctrlfs/src/lib.rs @@ -0,0 +1,414 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::io::BufRead; +use std::io::BufReader; +use std::io::ErrorKind; +use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; + +use nix::sys::statfs::fstatfs; +use nix::sys::statfs::RDTGROUP_SUPER_MAGIC; +use openat::Dir; +use openat::SimpleType; +use thiserror::Error; + +mod types; +pub use types::*; + +#[cfg(test)] +mod test; + +pub const DEFAULT_RESCTRL_ROOT: &str = "/sys/fs/resctrl"; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Invalid file format: {0:?}")] + InvalidFileFormat(PathBuf), + #[error("{1:?}: {0:?}")] + IoError(PathBuf, #[source] std::io::Error), + #[error("Unexpected line ({1}) in file: {0:?}")] + UnexpectedLine(PathBuf, String), + #[error("Not resctrl filesystem: {0:?}")] + NotResctrl(PathBuf), +} + +pub type Result<T> = std::result::Result<T, Error>; + +/// resctrlfs can give us a NotFound for various files and directories. In a lot of cases, these +/// are expected (e.g. when control or monitoring are disabled). Thus we translate these errors to +/// `None`. +fn wrap<S: Sized>(v: std::result::Result<S, Error>) -> std::result::Result<Option<S>, Error> { + if let Err(Error::IoError(_, ref e)) = v { + if e.kind() == std::io::ErrorKind::NotFound { + return Ok(None); + } + if e.kind() == std::io::ErrorKind::Other { + if let Some(errno) = e.raw_os_error() { + if errno == /* ENODEV */ 19 { + // If the resctrl group is removed after a control file is opened, + // ENODEV may returned. Ignore it. + return Ok(None); + } + } + } + } + v.map(Some) +} + +/// Parse a node range and return the set of nodes. This is either a range "x-y" +/// or a single value "x". +fn parse_node_range(s: &str) -> std::result::Result<BTreeSet<u32>, String> { + fn parse_node(s: &str) -> std::result::Result<u32, String> { + s.parse() + .map_err(|_| format!("id must be non-negative int: {}", s)) + } + match s.split_once('-') { + Some((first, last)) => { + let first = parse_node(first)?; + let last = parse_node(last)?; + if first > last { + return Err(format!("Invalid range: {}", s)); + } + Ok((first..(last + 1)).collect()) + } + None => Ok(BTreeSet::from([parse_node(s)?])), + } +} + +/// Parse a node range list (this is the format for resctrl cpus_list file and +/// also the format for cpusets in cgroupfs). e.g. "0-2,4" would return the set +/// {0, 1, 2, 4}. +fn nodes_from_str(s: &str) -> std::result::Result<BTreeSet<u32>, String> { + let mut nodes = BTreeSet::new(); + if s.is_empty() { + return Ok(nodes); + } + for range_str in s.split(',') { + let mut to_append = parse_node_range(range_str)?; + nodes.append(&mut to_append); + } + Ok(nodes) +} + +/// Format a set of nodes as a node range list. This is the inverse of +/// `nodes_to_str`. +fn fmt_nodes(f: &mut std::fmt::Formatter<'_>, nodes: &BTreeSet<u32>) -> std::fmt::Result { + fn print_range( + f: &mut std::fmt::Formatter<'_>, + range_start: u32, + range_end: u32, + ) -> std::fmt::Result { + if range_start == range_end { + write!(f, "{}", range_start) + } else { + write!(f, "{}-{}", range_start, range_end) + } + } + + let mut range_start = *nodes.iter().next().unwrap_or(&u32::MAX); + let mut range_end = range_start; + for cpu in nodes { + if range_end + 1 == *cpu || range_end == *cpu { + range_end = *cpu; + } else { + print_range(f, range_start, range_end)?; + write!(f, ",")?; + range_start = *cpu; + range_end = *cpu; + } + } + if !nodes.is_empty() { + print_range(f, range_start, range_end)?; + } + Ok(()) +} + +impl FromStr for Cpuset { + type Err = String; + fn from_str(s: &str) -> std::result::Result<Self, String> { + Ok(Cpuset { + cpus: nodes_from_str(s)?, + }) + } +} + +impl std::fmt::Display for Cpuset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_nodes(f, &self.cpus) + } +} + +impl FromStr for GroupMode { + type Err = String; + fn from_str(s: &str) -> std::result::Result<Self, String> { + match s { + "shareable" => Ok(GroupMode::Shareable), + "exclusive" => Ok(GroupMode::Exclusive), + _ => Err(format!("Unknown group mode: {}", s)), + } + } +} + +impl std::fmt::Display for GroupMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GroupMode::Shareable => write!(f, "shareable"), + GroupMode::Exclusive => write!(f, "exclusive"), + } + } +} + +impl FromStr for RmidBytes { + type Err = String; + fn from_str(s: &str) -> std::result::Result<Self, String> { + match s { + "Unavailable" => Ok(RmidBytes::Unavailable), + _ => Ok(RmidBytes::Bytes(s.parse().map_err(|_| "Not a number")?)), + } + } +} + +/// A reader for a resctrl MON or CTRL_MON or root group. +struct ResctrlGroupReader { + path: PathBuf, + dir: Dir, +} + +/// Reader to read entire resctrl hierarchy. +pub struct ResctrlReader { + path: PathBuf, +} + +impl ResctrlGroupReader { + /// Create a new reader for a resctrl MON or CTRL_MON or root group. + fn new(path: PathBuf) -> Result<ResctrlGroupReader> { + let dir = Dir::open(&path).map_err(|e| Error::IoError(path.clone(), e))?; + Ok(ResctrlGroupReader { path, dir }) + } + + /// Return the name of the group. + fn name(&self) -> String { + self.path + .file_name() + .expect("Unexpected .. in path") + .to_string_lossy() + .to_string() + } + + /// Read a value from a file that has a single line. If the file is empty, + /// the value will be derived from an empty string. + fn read_empty_or_singleline_file<T: FromStr>(&self, file_name: &str) -> Result<T> { + let file = self + .dir + .open_file(file_name) + .map_err(|e| self.io_error(file_name, e))?; + let buf_reader = BufReader::new(file); + let line = buf_reader + .lines() + .next() + .unwrap_or_else(|| Ok("".to_owned())); + let line = line.map_err(|e| self.io_error(file_name, e))?; + line.parse::<T>() + .map_err(move |_| self.unexpected_line(file_name, line)) + } + + /// Read a value from a file that has a single line. If the file is empty, + /// InvalidFileFormat is returned. + fn read_singleline_file<T: FromStr>(&self, file_name: &str) -> Result<T> { + let file = self + .dir + .open_file(file_name) + .map_err(|e| self.io_error(file_name, e))?; + let buf_reader = BufReader::new(file); + if let Some(line) = buf_reader.lines().next() { + let line = line.map_err(|e| self.io_error(file_name, e))?; + return line + .parse::<T>() + .map_err(move |_| self.unexpected_line(file_name, line)); + } + Err(self.invalid_file_format(file_name)) + } + + /// Helper to create InvalidFileFormat error + fn invalid_file_format<P: AsRef<Path>>(&self, file_name: P) -> Error { + let mut p = self.path.clone(); + p.push(file_name); + Error::InvalidFileFormat(p) + } + + /// Helper to create IoError error + fn io_error<P: AsRef<Path>>(&self, file_name: P, e: std::io::Error) -> Error { + let mut p = self.path.clone(); + p.push(file_name); + Error::IoError(p, e) + } + + /// Helper to create UnexpectedLine error + fn unexpected_line<P: AsRef<Path>>(&self, file_name: P, line: String) -> Error { + let mut p = self.path.clone(); + p.push(file_name); + Error::UnexpectedLine(p, line) + } + + /// Return L3 cache ID for given mon_stat_dir name. e.g. "mon_L3_01" returns 1. + fn maybe_get_l3_mon_stat_dir_id(&self) -> Result<u64> { + let name = self.name(); + if !name.starts_with("mon_L3_") { + return Err(self.invalid_file_format("")); + } + name[7..] + .parse::<u64>() + .map_err(|_| self.invalid_file_format("")) + } + + /// Read the inode number of the group. + fn read_inode_number(&self) -> Result<u64> { + let meta = self.dir.metadata(".").map_err(|e| self.io_error("", e))?; + Ok(meta.stat().st_ino) + } + + /// Read cpuset from cpus_list file + fn read_cpuset(&self) -> Result<Cpuset> { + self.read_empty_or_singleline_file("cpus_list") + } + + /// Read mode file. Only applicable for CTRL_MON and root group. + fn read_mode(&self) -> Result<GroupMode> { + self.read_singleline_file("mode") + } + + /// Read all L3_mon data for this group. + fn read_l3_mon_stat(&self) -> Result<L3MonStat> { + Ok(L3MonStat { + llc_occupancy_bytes: wrap(self.read_singleline_file("llc_occupancy"))?, + mbm_total_bytes: wrap(self.read_singleline_file("mbm_total_bytes"))?, + mbm_local_bytes: wrap(self.read_singleline_file("mbm_local_bytes"))?, + }) + } + + /// Read mon_stat directory if it exists otherwise return None. + fn read_mon_stat(&self) -> Result<MonStat> { + Ok(MonStat { + l3_mon_stat: Some( + self.child_iter("mon_data".into())? + .flat_map(|child| { + child + .read_l3_mon_stat() + .map(|v| child.maybe_get_l3_mon_stat_dir_id().map(|id| (id, v))) + }) + .collect::<Result<BTreeMap<_, _>>>()?, + ), + }) + } + + /// Read current group as a MON group + fn read_mon_group(&self) -> Result<MonGroupStat> { + Ok(MonGroupStat { + inode_number: Some(self.read_inode_number()?), + cpuset: Some(self.read_cpuset()?), + mon_stat: wrap(self.read_mon_stat())?, + }) + } + + /// Read current group as a CTRL_MON group + fn read_ctrl_mon_group(&self) -> Result<CtrlMonGroupStat> { + Ok(CtrlMonGroupStat { + inode_number: Some(self.read_inode_number()?), + cpuset: Some(self.read_cpuset()?), + mode: wrap(self.read_mode())?, + mon_stat: wrap(self.read_mon_stat())?, + mon_groups: wrap(self.read_child_mon_groups())?, + }) + } + + /// Get iterator of child group readers + fn child_iter( + &self, + child_dir_name: PathBuf, + ) -> Result<impl Iterator<Item = ResctrlGroupReader> + '_> { + Ok(self + .dir + .list_dir(&child_dir_name) + .map_err(|e| self.io_error(&child_dir_name, e))? + .filter_map(move |entry| match entry { + Ok(entry) if entry.simple_type() == Some(SimpleType::Dir) => { + let relative_path = child_dir_name.join(entry.file_name()); + let sub_dir = match self.dir.sub_dir(relative_path.as_path()) { + Ok(d) => d, + Err(_) => return None, + }; + let mut path = self.path.clone(); + path.push(entry.file_name()); + Some(ResctrlGroupReader { path, dir: sub_dir }) + } + _ => None, + })) + } + + /// Read child MON groups + fn read_child_mon_groups(&self) -> Result<BTreeMap<String, MonGroupStat>> { + self.child_iter("mon_groups".into())? + .map(|child| child.read_mon_group().map(|v| (child.name(), v))) + .collect::<Result<BTreeMap<_, _>>>() + } + + /// Read child CTRL MON groups + fn read_child_ctrl_mon_groups(&self) -> Result<BTreeMap<String, CtrlMonGroupStat>> { + self.child_iter(".".into())? + .filter(|r| !["info", "mon_groups", "mon_data"].contains(&r.name().as_str())) + .map(|child| child.read_ctrl_mon_group().map(|v| (child.name(), v))) + .collect::<Result<BTreeMap<_, _>>>() + } +} + +impl ResctrlReader { + pub fn new(path: PathBuf, validate: bool) -> Result<ResctrlReader> { + let dir = Dir::open(&path).map_err(|e| Error::IoError(path.clone(), e))?; + // Check that it's a resctrl fs + if validate { + let statfs = match fstatfs(&dir) { + Ok(s) => s, + Err(e) => { + return Err(Error::IoError( + path, + std::io::Error::new(ErrorKind::Other, format!("Failed to fstatfs: {}", e)), + )); + } + }; + + if statfs.filesystem_type() != RDTGROUP_SUPER_MAGIC { + return Err(Error::NotResctrl(path)); + } + } + Ok(ResctrlReader { path }) + } + + pub fn root() -> Result<ResctrlReader> { + Self::new(DEFAULT_RESCTRL_ROOT.into(), true) + } + + pub fn read_all(&self) -> Result<ResctrlSample> { + let reader = ResctrlGroupReader::new(self.path.clone())?; + Ok(ResctrlSample { + cpuset: Some(reader.read_cpuset()?), + mode: wrap(reader.read_mode())?, + mon_stat: wrap(reader.read_mon_stat())?, + ctrl_mon_groups: Some(reader.read_child_ctrl_mon_groups()?), + mon_groups: wrap(reader.read_child_mon_groups())?, + }) + } +} diff --git a/below/resctrlfs/src/test.rs b/below/resctrlfs/src/test.rs new file mode 100644 index 00000000..4d7bd02e --- /dev/null +++ b/below/resctrlfs/src/test.rs @@ -0,0 +1,537 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ffi::OsStr; +use std::fs::create_dir_all; +use std::fs::File; +use std::io::Write; +use std::os::linux::fs::MetadataExt; +use std::path::Path; +use std::path::PathBuf; + +use maplit::btreemap; +use maplit::btreeset; +use paste::paste; +use tempfile::TempDir; + +use crate::*; + +macro_rules! test_success { + ($name:ident, $filename:literal, $contents:literal, $expected_val:stmt, $suffix:ident) => { + paste! { + #[test] + fn [<test_ $name _success_ $suffix>]() { + let test_group = TestGenericGroup::new(); + let reader = ResctrlGroupReader::new(test_group.path()) + .expect("Failed to create reader"); + test_group.create_file_with_content($filename, $contents); + let val = reader + .$name() + .expect(concat!("Failed to read ", $filename)); + assert_eq!(val, {$expected_val}); + } + } + }; + ($name:ident, $filename:literal, $contents:literal, $expected_val:stmt) => { + test_success!($name, $filename, $contents, $expected_val, ""); + }; +} + +macro_rules! test_failure { + ($name:ident, $filename:literal, $err_contents:literal, $suffix:ident) => { + paste! { + #[test] + fn [<test_ $name _failure_ $suffix>]() { + let test_group = TestGenericGroup::new(); + let reader = ResctrlGroupReader::new(test_group.path()) + .expect("Failed to create reader"); + test_group.create_file_with_content($filename, $err_contents); + let val = reader.$name(); + assert!(val.is_err()); + } + } + }; + ($name:ident, $filename:literal, $err_contents:literal) => { + test_failure!($name, $filename, $err_contents, ""); + }; +} + +trait TestGroupCommon { + fn path(&self) -> PathBuf; + + fn create_child_dir<P: AsRef<Path>>(&self, p: P) -> PathBuf { + let path = self.path().join(p); + std::fs::create_dir(&path) + .unwrap_or_else(|_| panic!("Failed to create child dir {}", path.display())); + path + } + + fn create_file_with_content<P: AsRef<Path>>(&self, p: P, content: &[u8]) { + let path = self.path().join(p); + create_dir_all(path.parent().unwrap()).unwrap(); + let mut file = + File::create(&path).unwrap_or_else(|_| panic!("Failed to create {}", path.display())); + file.write_all(content) + .unwrap_or_else(|_| panic!("Failed to write to {}", path.display())); + } + + fn set_cpus_list(&self, list: &[u8]) { + self.create_file_with_content(OsStr::new("cpus_list"), list); + } + + fn set_mode(&self, mode: &[u8]) { + self.create_file_with_content(OsStr::new("mode"), mode); + } +} + +struct TestResctrlfs { + tempdir: TempDir, + ctrl_mon: TestCtrlMonGroup, +} + +struct TestCtrlMonGroup { + path: PathBuf, +} + +struct TestMonGroup { + path: PathBuf, +} + +struct TestGenericGroup { + tempdir: TempDir, +} + +impl TestGroupCommon for TestResctrlfs { + fn path(&self) -> PathBuf { + self.tempdir.path().to_path_buf() + } +} + +impl TestResctrlfs { + fn new() -> TestResctrlfs { + let tempdir = TempDir::new().expect("Failed to create tempdir"); + let ctrl_mon = TestCtrlMonGroup::new(tempdir.path().to_path_buf()); + TestResctrlfs { tempdir, ctrl_mon } + } + + fn initialize(&self) { + self.create_child_dir(OsStr::new("info")); + self.ctrl_mon.initialize(b"0-7\n", b"shareable\n"); + } + + fn create_child_ctrl_mon<P: AsRef<Path>>(&self, p: P) -> TestCtrlMonGroup { + let path = self.create_child_dir(p); + TestCtrlMonGroup::new(path) + } + + fn create_child_mon_group<P: AsRef<Path>>(&self, p: P) -> TestMonGroup { + let path = self.create_child_dir(PathBuf::from(OsStr::new("mon_groups")).join(p)); + TestMonGroup::new(path) + } +} + +impl TestGroupCommon for TestCtrlMonGroup { + fn path(&self) -> PathBuf { + self.path.clone() + } +} + +impl TestCtrlMonGroup { + fn new(path: PathBuf) -> TestCtrlMonGroup { + TestCtrlMonGroup { path } + } + + fn initialize(&self, cpus_list: &[u8], mode: &[u8]) { + self.set_cpus_list(cpus_list); + self.set_mode(mode); + self.create_child_dir(OsStr::new("mon_data")); + self.create_child_dir(OsStr::new("mon_groups")); + } + + fn create_child_mon_group<P: AsRef<Path>>(&self, p: P) -> TestMonGroup { + let path = self.create_child_dir(PathBuf::from(OsStr::new("mon_groups")).join(p)); + TestMonGroup::new(path) + } +} + +impl TestGroupCommon for TestMonGroup { + fn path(&self) -> PathBuf { + self.path.clone() + } +} + +impl TestMonGroup { + fn new(path: PathBuf) -> TestMonGroup { + TestMonGroup { path } + } + + fn initialize(&self, cpus_list: &[u8]) { + self.set_cpus_list(cpus_list); + self.create_child_dir(OsStr::new("mon_data")); + self.create_child_dir(OsStr::new("mon_groups")); + } +} + +impl TestGenericGroup { + fn new() -> TestGenericGroup { + let tempdir = TempDir::new().expect("Failed to create tempdir"); + TestGenericGroup { tempdir } + } +} + +impl TestGroupCommon for TestGenericGroup { + fn path(&self) -> PathBuf { + self.tempdir.path().to_path_buf() + } +} + +#[test] +fn test_resctrlfs_read_empty() { + let resctrlfs = TestResctrlfs::new(); + resctrlfs.initialize(); + let reader = ResctrlReader::new(resctrlfs.path().to_path_buf(), false) + .expect("Failed to construct reader"); + reader.read_all().expect("Failed to read all"); +} + +#[test] +fn test_resctrlfs_read_simple() { + let resctrlfs = TestResctrlfs::new(); + { + // Set up filesystem + resctrlfs.initialize(); + resctrlfs.create_file_with_content(OsStr::new("mon_data/mon_L3_00/llc_occupancy"), b"0\n"); + resctrlfs.create_file_with_content(OsStr::new("mon_data/mon_L3_11/llc_occupancy"), b"11\n"); + resctrlfs + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_total_bytes"), b"100\n"); + resctrlfs + .create_file_with_content(OsStr::new("mon_data/mon_L3_11/mbm_total_bytes"), b"111\n"); + resctrlfs + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_local_bytes"), b"200\n"); + resctrlfs + .create_file_with_content(OsStr::new("mon_data/mon_L3_11/mbm_local_bytes"), b"211\n"); + + let ctrl_mon_1 = resctrlfs.create_child_ctrl_mon(OsStr::new("ctrl_mon_1")); + ctrl_mon_1.initialize(b"0-3\n", b"shareable\n"); + ctrl_mon_1.create_file_with_content(OsStr::new("mon_data/mon_L3_00/llc_occupancy"), b"0\n"); + ctrl_mon_1 + .create_file_with_content(OsStr::new("mon_data/mon_L3_12/llc_occupancy"), b"11\n"); + ctrl_mon_1 + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_total_bytes"), b"100\n"); + ctrl_mon_1 + .create_file_with_content(OsStr::new("mon_data/mon_L3_12/mbm_total_bytes"), b"111\n"); + ctrl_mon_1 + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_local_bytes"), b"200\n"); + ctrl_mon_1 + .create_file_with_content(OsStr::new("mon_data/mon_L3_12/mbm_local_bytes"), b"211\n"); + + let _ctrl_mon_2 = resctrlfs + .create_child_ctrl_mon(OsStr::new("ctrl_mon_2")) + .initialize(b"4-5\n", b"exclusive\n"); + + let inner_mon = ctrl_mon_1.create_child_mon_group(OsStr::new("mon_1")); + inner_mon.initialize(b"1-2\n"); + inner_mon.create_file_with_content(OsStr::new("mon_data/mon_L3_00/llc_occupancy"), b"0\n"); + inner_mon.create_file_with_content(OsStr::new("mon_data/mon_L3_13/llc_occupancy"), b"11\n"); + inner_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_total_bytes"), b"100\n"); + inner_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_13/mbm_total_bytes"), b"111\n"); + inner_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_local_bytes"), b"200\n"); + inner_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_13/mbm_local_bytes"), b"211\n"); + + let top_level_mon = resctrlfs.create_child_mon_group(OsStr::new("mon_0")); + top_level_mon.initialize(b"0-1\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/llc_occupancy"), b"0\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_14/llc_occupancy"), b"11\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_total_bytes"), b"100\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_14/mbm_total_bytes"), b"111\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_00/mbm_local_bytes"), b"200\n"); + top_level_mon + .create_file_with_content(OsStr::new("mon_data/mon_L3_14/mbm_local_bytes"), b"211\n"); + } + + let reader = ResctrlReader::new(resctrlfs.path().to_path_buf(), false) + .expect("Failed to construct reader"); + let sample = reader.read_all().expect("Failed to read all"); + assert_eq!(sample.mode, Some(GroupMode::Shareable)); + assert_eq!( + sample.cpuset, + Some(Cpuset { + cpus: btreeset! {0, 1, + 2, 3, 4, 5, 6, 7} + }) + ); + assert_eq!( + sample.mon_stat, + Some(MonStat { + l3_mon_stat: Some(btreemap! { + 0 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(0)), + mbm_total_bytes: Some(RmidBytes::Bytes(100)), + mbm_local_bytes: Some(RmidBytes::Bytes(200)) + }, + 11 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(11)), + mbm_total_bytes: Some(RmidBytes::Bytes(111)), + mbm_local_bytes: Some(RmidBytes::Bytes(211)) + } + }) + }) + ); + assert!(sample.ctrl_mon_groups.is_some()); + assert_eq!(sample.ctrl_mon_groups.as_ref().unwrap().len(), 2); + + let ctrl_mon_1 = &sample.ctrl_mon_groups.as_ref().unwrap()["ctrl_mon_1"]; + assert!(ctrl_mon_1.inode_number.is_some()); + assert_eq!(ctrl_mon_1.mode, Some(GroupMode::Shareable)); + assert_eq!( + ctrl_mon_1.cpuset, + Some(Cpuset { + cpus: btreeset! {0,1,2,3} + }) + ); + assert_eq!( + ctrl_mon_1.mon_stat, + Some(MonStat { + l3_mon_stat: Some(btreemap! { + 0 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(0)), + mbm_total_bytes: Some(RmidBytes::Bytes(100)), + mbm_local_bytes: Some(RmidBytes::Bytes(200)) + }, + 12 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(11)), + mbm_total_bytes: Some(RmidBytes::Bytes(111)), + mbm_local_bytes: Some(RmidBytes::Bytes(211)) + } + }) + }) + ); + + let ctrl_mon_2 = &sample.ctrl_mon_groups.as_ref().unwrap()["ctrl_mon_2"]; + assert!(ctrl_mon_2.inode_number.is_some()); + assert_eq!(ctrl_mon_2.mode, Some(GroupMode::Exclusive)); + assert_eq!( + ctrl_mon_2.cpuset, + Some(Cpuset { + cpus: btreeset! {4,5} + }) + ); + assert_eq!( + ctrl_mon_2.mon_stat, + Some(MonStat { + l3_mon_stat: Some(btreemap! {}) + }) + ); + + let inner_mon = &ctrl_mon_1.mon_groups.as_ref().unwrap()["mon_1"]; + assert!(inner_mon.inode_number.is_some()); + assert_eq!( + inner_mon.cpuset, + Some(Cpuset { + cpus: btreeset! {1,2} + }) + ); + assert_eq!( + inner_mon.mon_stat, + Some(MonStat { + l3_mon_stat: Some(btreemap! { + 0 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(0)), + mbm_total_bytes: Some(RmidBytes::Bytes(100)), + mbm_local_bytes: Some(RmidBytes::Bytes(200)) + }, + 13 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(11)), + mbm_total_bytes: Some(RmidBytes::Bytes(111)), + mbm_local_bytes: Some(RmidBytes::Bytes(211)) + } + }) + }) + ); + + let top_level_mon = &sample.mon_groups.as_ref().unwrap()["mon_0"]; + assert!(top_level_mon.inode_number.is_some()); + assert_eq!( + top_level_mon.cpuset, + Some(Cpuset { + cpus: btreeset! {0, 1} + }) + ); + assert_eq!( + top_level_mon.mon_stat, + Some(MonStat { + l3_mon_stat: Some(btreemap! { + 0 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(0)), + mbm_total_bytes: Some(RmidBytes::Bytes(100)), + mbm_local_bytes: Some(RmidBytes::Bytes(200)) + }, + 14 => L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(11)), + mbm_total_bytes: Some(RmidBytes::Bytes(111)), + mbm_local_bytes: Some(RmidBytes::Bytes(211)) + } + }) + }) + ); +} + +#[test] +fn test_read_inode_number() { + let group = TestGenericGroup::new(); + let reader = ResctrlGroupReader::new(group.path()).expect("Failed to construct reader"); + let inode = reader + .read_inode_number() + .expect("Failed to read inode number"); + assert_eq!( + inode, + std::fs::metadata(group.path()) + .expect("Failed to read inode number with fs::metadata") + .st_ino() + ); +} + +test_success!( + read_cpuset, + "cpus_list", + b"", + Cpuset { + cpus: BTreeSet::new() + }, + empty_file +); +test_success!( + read_cpuset, + "cpus_list", + b"\n", + Cpuset { + cpus: BTreeSet::new() + }, + single_empty_line +); +test_success!( + read_cpuset, + "cpus_list", + b"1\n", + Cpuset { + cpus: BTreeSet::from([1]) + }, + single_cpu +); +test_success!( + read_cpuset, + "cpus_list", + b"1,3-5\n", + Cpuset { + cpus: BTreeSet::from([1, 3, 4, 5]) + }, + multi_cpu_with_range +); +test_failure!(read_cpuset, "cpus_list", b"-1\n", negative_cpu); +test_failure!(read_cpuset, "cpus_list", b"c\n", invalid_char); + +test_success!( + read_mode, + "mode", + b"exclusive\n", + GroupMode::Exclusive, + exclusive +); +test_success!( + read_mode, + "mode", + b"shareable\n", + GroupMode::Shareable, + shareable +); +test_failure!(read_mode, "mode", b"invalid_mode\n", invalid); +test_failure!(read_mode, "mode", b"\n", empty); + +test_success!( + read_l3_mon_stat, + "llc_occupancy", + b"123456789\n", + L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Bytes(123456789)), + ..Default::default() + }, + llc_occupancy +); +test_success!( + read_l3_mon_stat, + "llc_occupancy", + b"Unavailable\n", + L3MonStat { + llc_occupancy_bytes: Some(RmidBytes::Unavailable), + ..Default::default() + }, + llc_occupancy_unavailable +); +test_failure!( + read_l3_mon_stat, + "llc_occupancy", + b"-1\n", + llc_occupancy_negative +); + +test_success!( + read_l3_mon_stat, + "mbm_total_bytes", + b"123\n", + L3MonStat { + mbm_total_bytes: Some(RmidBytes::Bytes(123)), + ..Default::default() + }, + mbm_total_bytes +); +test_success!( + read_l3_mon_stat, + "mbm_total_bytes", + b"Unavailable\n", + L3MonStat { + mbm_total_bytes: Some(RmidBytes::Unavailable), + ..Default::default() + }, + mbm_total_bytes_unavailable +); + +test_success!( + read_l3_mon_stat, + "mbm_local_bytes", + b"123\n", + L3MonStat { + mbm_local_bytes: Some(RmidBytes::Bytes(123)), + ..Default::default() + }, + mbm_local_bytes +); +test_success!( + read_l3_mon_stat, + "mbm_local_bytes", + b"Unavailable\n", + L3MonStat { + mbm_local_bytes: Some(RmidBytes::Unavailable), + ..Default::default() + }, + mbm_local_bytes_unavailable +); |