summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Chen <brianc118@meta.com>2023-12-06 10:17:58 -0800
committerFacebook GitHub Bot <facebook-github-bot@users.noreply.github.com>2023-12-06 10:17:58 -0800
commit5151c5a2dc07e3fcf9b3bf2a4416c67a631c1c08 (patch)
tree0b206381306a17acc14e56ad1dbd57d364854275
parentba700bd2d01bcbc160ce6848e481d37d1940a63b (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.toml1
-rw-r--r--below/resctrlfs/Cargo.toml22
-rw-r--r--below/resctrlfs/src/lib.rs414
-rw-r--r--below/resctrlfs/src/test.rs537
-rw-r--r--below/resctrlfs/src/types.rs89
5 files changed, 1063 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
index d2aac591..905c1106 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
+);