blob: 53fd64e8a6ad61e71d5e167919de0004e3993911 [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::collections::HashSet;
use std::fmt::Display;
use std::fs::read_to_string;
use std::path::Path;
use std::slice::Iter;
use std::str::FromStr;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use glob::glob;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::arch;
use crate::common::read_from_file;
pub const SMT_CONTROL_PATH: &str = "sys/devices/system/cpu/smt/control";
pub const CPU_ONLINE_PATH: &str = "sys/devices/system/cpu/online";
const ROOT_CPUSET_CPUS_PATH: &str = "sys/fs/cgroup/cpuset/cpus";
const ISOLATED_CPUSET_PATH: &str = "sys/devices/system/cpu/isolated";
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum HotplugCpuAction {
// Set all CPUs to online.
OnlineAll,
// Offline small CPUs if the device has big/little clusters and the number
// of active big cores meets the minimum active threads.
OfflineSmallCore { min_active_threads: u32 },
// Offline SMT cores if available and the number of physical cores meets
// the minimum active therads.
OfflineSMT { min_active_threads: u32 },
// Offline half of CPUs and ensuring at least min_active_threads remain active.
OfflineHalf { min_active_threads: u32 },
}
/// The set of cpu core numbers
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cpuset(Vec<usize>);
impl Cpuset {
// Parses a cpuset string: that is, a comma-separated list of ranges, where
// each range is either in the form "l-u" or "c", where l, u, and c are
// positive integers. Example: 0-3,9,11-12.
fn parse(cpuset_str: &str) -> Result<Self> {
static CPUSET_RANGE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(\d+)-(\d+)$").expect("bad cpuset range RE"));
let mut cores: Vec<usize> = vec![];
// Check for a single newline, because trim() won't remove it.
if cpuset_str.is_empty() || cpuset_str == "\n" {
return Ok(Cpuset(cores));
}
let ranges = cpuset_str.trim().split(',');
for range in ranges {
if let Some(m) = CPUSET_RANGE_RE.captures(range) {
// No errors expected here.
let lower = m[1].parse::<usize>().expect("parse/RE mismatch 1");
let upper = m[2].parse::<usize>().expect("parse/RE mismatch 2");
if lower > upper {
bail!("bad range '{range}' in cpuset '{cpuset_str}'");
}
for x in lower..=upper {
cores.push(x);
}
} else {
cores.push(
range
.parse::<usize>()
.with_context(|| format!("malformed cpuset: '{cpuset_str}'"))?,
)
}
}
// Sanity check.
for i in 0..cores.len() - 1 {
if cores[i] >= cores[i + 1] {
bail!("cpuset '{cpuset_str}' has overlapping or out-of-order CPUs");
}
}
Ok(Cpuset(cores))
}
fn difference(&self, other: &Self) -> Self {
let mut result: Vec<usize> = vec![];
if other.len() == 0 {
self.clone()
} else {
// Quadratic but short.
for cpu in self.iter() {
if !other.iter().any(|other| cpu == other) {
result.push(*cpu);
}
}
Cpuset(result)
}
}
// Returns the cpuset of cores isolated by the "isolcpus" kernel command
// line option. See
// https://docs.kernel.org/admin-guide/kernel-parameters.html#cpu-lists.
// Note that the "isolcpus" feature is deprecated and may go away.
// However, it is still more convenient than cpusets for some use cases.
fn isolated_cores(root: &Path) -> Result<Self> {
let path = root.join(ISOLATED_CPUSET_PATH);
// Tolerate missing sysfs entry.
if !path.exists() {
Ok(Cpuset(vec![]))
} else {
let isolated_str = read_to_string(path).context("failed to get isolated cores")?;
Self::parse(&isolated_str).context("failed to parse isolated cores")
}
}
pub fn all_cores(root: &Path) -> Result<Self> {
let cpuset_str = read_to_string(root.join(ROOT_CPUSET_CPUS_PATH))
.context("Failed to get root cpuset")?;
Ok(Self::parse(&cpuset_str)
.context("failed to parse root cpuset")?
.difference(&Self::isolated_cores(root).context("failed to compute all cores")?))
}
pub fn little_cores(root: &Path) -> Result<Self> {
for property in [
"cpu_capacity",
"cpufreq/cpuinfo_max_freq",
"acpi_cppc/highest_perf",
] {
if let Some(cpuset) = get_cpus_with_min_property(root, property)? {
return Ok(cpuset.difference(
&Self::isolated_cores(root).context("failed to compute little cores")?,
));
}
}
Self::all_cores(root)
}
pub fn online_cpus(root: &Path) -> Result<Self> {
Self::parse(read_to_string(root.join(CPU_ONLINE_PATH))?.trim_end_matches('\n'))
}
/// The number of cpu cores in this [Cpuset].
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> Iter<usize> {
self.0.iter()
}
}
impl FromIterator<usize> for Cpuset {
fn from_iter<T>(cpus: T) -> Self
where
T: IntoIterator<Item = usize>,
{
Self(cpus.into_iter().collect())
}
}
impl Display for Cpuset {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let vector = &self.0;
let length = vector.len();
// "lower" tracks the index of the lower bound in a range.
let mut lower = 0;
for upper in 0..length {
if upper + 1 < length && vector[upper + 1] == vector[upper] + 1 {
// When there is a next value, and it is consecutive, continue
// advancing the range upper bound.
continue;
}
if lower > 0 {
// At least one range was already output.
write!(formatter, ",")?;
}
if upper > lower {
write!(formatter, "{}-{}", vector[lower], vector[upper])?;
} else {
write!(formatter, "{}", vector[upper])?;
}
// Advance lower to the next array index.
lower = upper + 1;
}
Ok(())
}
}
/// Returns [Cpuset] containing cpus with the minimal value of the property.
/// If there is more than 2 property values, this returns the cpus with
/// two smallest values of the property.
///
/// Returns [None] if:
///
/// * The property file does not exist or
/// * All cpus have the same property value.
///
/// The properties are read from /sys/bus/cpu/devices/cpu*/{property name}.
/// E.g., this function returns "0,1" for the following cpu properties.
/// | cpu # | property value |
/// |-------|----------------|
/// | 0 | 512 |
/// | 1 | 512 |
/// | 2 | 1024 |
/// | 3 | 1024 |
fn get_cpus_with_min_property(root: &Path, property: &str) -> Result<Option<Cpuset>> {
if !root
.join("sys/bus/cpu/devices/cpu0")
.join(property)
.exists()
{
return Ok(None);
}
let cpu_pattern = root
.join("sys/bus/cpu/devices/cpu*")
.to_str()
.context("Failed to construct cpu pattern string")?
.to_owned();
let cpu_pattern_prefix = root
.join("sys/bus/cpu/devices/cpu")
.to_str()
.context("Failed to construct cpu path prefix")?
.to_owned();
let cpu_properties = glob(&cpu_pattern)?
.map(|cpu_dir| {
let cpu_dir = cpu_dir?;
let cpu_number: usize = cpu_dir
.to_str()
.context("Failed to convert cpu path to string")?
.strip_prefix(&cpu_pattern_prefix)
.context("Failed to strip prefix")?
.parse()?;
let property_path = Path::new(&cpu_dir).join(property);
if property_path.exists() {
Ok(Some((cpu_number, read_from_file(&property_path)?)))
} else {
Ok(None)
}
})
.filter_map(|result: Result<Option<(usize, u64)>>| match result {
Ok(Some(v)) => Some(Ok(v)),
Ok(None) => None,
Err(e) => Some(Err(e)),
})
.collect::<Result<Vec<(usize, u64)>>>()?;
let mut properties: Vec<u64> = cpu_properties.iter().map(|(_, prop)| *prop).collect();
properties.sort();
properties.dedup();
if properties.len() <= 1 {
return Ok(None);
}
// If the map has more than 2 attribute values, we consider the cpus with
// two smallest capacities / freqs as small cores.
let small_core_limit = if properties.len() > 2 {
properties[1]
} else {
properties[0]
};
let cpuset = cpu_properties
.into_iter()
.filter(|(_, prop)| *prop <= small_core_limit)
.map(|(cpu, _)| cpu)
.collect::<Cpuset>();
Ok(Some(cpuset))
}
pub fn write_to_cpu_policy_patterns(pattern: &str, new_value: &str) -> Result<()> {
let mut applied: bool = false;
let entries: Vec<_> = glob(pattern)?.collect();
if entries.is_empty() {
applied = true;
}
for entry in entries {
let policy_path = entry?;
let mut affected_cpus_path = policy_path.to_path_buf();
affected_cpus_path.set_file_name("affected_cpus");
// Skip the policy update if there are no CPUs can be affected by policy.
// Otherwise, write to the scaling governor may cause error.
if affected_cpus_path.exists() {
if let Ok(affected_cpus) = read_to_string(affected_cpus_path) {
if affected_cpus.trim_end_matches('\n').is_empty() {
applied = true;
continue;
}
}
}
// Allow read fail due to CPU may be offlined.
if let Ok(current_value) = read_to_string(&policy_path) {
if current_value.trim_end_matches('\n') != new_value {
std::fs::write(&policy_path, new_value).with_context(|| {
format!(
"Failed to set attribute to {}, new value: {}",
policy_path.display(),
new_value
)
})?;
}
applied = true;
}
}
// Fail if there are entries in the pattern but nothing is applied
if !applied {
bail!("Failed to read any of the pattern {pattern}");
}
Ok(())
}
// Change a group of CPU online status through sysfs.
// * `cpus_fmt` - The format string of the target CPUs in either of the format:
// 1. a list separated by comma (,). e.g. 0,1,2,3 to set CPU 0,1,2,3
// 2. a range represented by hyphen (-). e.g. 0-3 to set CPU 0,1,2,3
// * `online` - Set true to online CUPs. Set false to offline CPUs.
fn update_cpu_online_status(root: &Path, cpuset: &Cpuset, online: bool) -> Result<()> {
let online_value = if online { "1" } else { "0" };
for cpu in cpuset.iter() {
let pattern = format!("sys/devices/system/cpu/cpu{cpu}/online");
let cpu_path = root.join(pattern);
if cpu_path.exists() {
std::fs::write(cpu_path, online_value.as_bytes())?;
}
}
Ok(())
}
// Simultaneous Multithreading(SMT) control is sysfs control interface
// in /sys/devices/system/cpu/smt/control
#[derive(Debug, PartialEq)]
enum SmtControlStatus {
// SMT is enabled
On,
// SMT is disabled
Off,
// SMT is force disabled. Cannot be changed.
ForceOff,
// SMT is not supported by the CPU
NotSupported,
// SMT runtime toggling is not implemented for the architecture
NotImplemented,
}
impl FromStr for SmtControlStatus {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim_end() {
"on" => Ok(SmtControlStatus::On),
"off" => Ok(SmtControlStatus::Off),
"forceoff" => Ok(SmtControlStatus::ForceOff),
"notsupported" => Ok(SmtControlStatus::NotSupported),
"notimplemented" => Ok(SmtControlStatus::NotImplemented),
_ => bail!("Unknown Smt Control Status: '{s}'"),
}
}
}
// Checks the SMT sysfs /sys/devices/system/cpu/smt/control is able to be controlled.
// The "on" and "off" are the two state can be controlled.
// Returns Ok(true) when smt control is "on" or "off"
fn is_smt_contrallable(root: &Path) -> Result<bool> {
let control_path = root.join(SMT_CONTROL_PATH);
if !control_path.exists() {
return Ok(false);
}
let control_string = read_to_string(&control_path)?;
match SmtControlStatus::from_str(&control_string)? {
SmtControlStatus::On | SmtControlStatus::Off => Ok(true),
_ => Ok(false),
}
}
// Change the SMT control state through sysfs.
// Reference: https://docs.kernel.org/admin-guide/hw-vuln/l1tf.html#smt-control
// * `enable`:
// Set true to enable SMT, which to online all CPUs. Try to access cpu online in sysfs has
// no restrictions.
// Set false to disable SMT, which to offline the non-primary CPUs. Try to set non-primary
// CPUs to online state in sysfs will be rejected.
fn update_cpu_smt_control(root: &Path, enable: bool) -> Result<()> {
let control = root.join(SMT_CONTROL_PATH);
if is_smt_contrallable(root)? {
if enable {
std::fs::write(control, "on")?;
} else {
std::fs::write(control, "off")?;
}
}
Ok(())
}
// Determines the number of physical cores on the system by analyzing the
// core_cpus_list files in sysfs. Sibling cores, sharing the same
// physical core, have identical core_cpus_list values. By identifying
// unique values, the function counts distinct physical cores.
fn get_physical_cores(root: &Path) -> Result<u32> {
let mut core_cpus = HashSet::new();
let core_cpus_list_pattern = root
.join("sys/devices/system/cpu/cpu*/topology/core_cpus_list")
.to_str()
.context("Failed to construct thread core cpus list pattern")?
.to_owned();
for core_cpus_list in glob(&core_cpus_list_pattern)? {
core_cpus.insert(read_to_string(core_cpus_list?)?);
}
Ok(core_cpus.len() as u32)
}
pub fn hotplug_cpus(root: &Path, action: HotplugCpuAction) -> Result<()> {
arch::platform_hotplug_cpus_pre_hook(action);
match action {
HotplugCpuAction::OnlineAll => {
let all_cores = Cpuset::all_cores(root)?;
update_cpu_smt_control(root, true)?;
update_cpu_online_status(root, &all_cores, true)?;
}
HotplugCpuAction::OfflineSmallCore { min_active_threads } => {
let little_cores = Cpuset::little_cores(root)?;
let all_cores = Cpuset::all_cores(root)?;
if all_cores.len() - little_cores.len() >= min_active_threads as usize {
update_cpu_online_status(root, &little_cores, false)?;
}
}
HotplugCpuAction::OfflineSMT { min_active_threads } => {
let physical_cores = get_physical_cores(root)?;
if physical_cores >= min_active_threads {
update_cpu_smt_control(root, false)?;
}
}
HotplugCpuAction::OfflineHalf { min_active_threads } => {
let all_cores = Cpuset::all_cores(root)?;
let n_all_cores = all_cores.len();
let last_core = n_all_cores - 1;
if n_all_cores > min_active_threads as usize {
let first_core = std::cmp::max((last_core / 2) + 1, min_active_threads as usize);
let half_cores = (first_core..=last_core).collect();
update_cpu_online_status(root, &half_cores, false)?;
}
}
}
arch::platform_hotplug_cpus_post_hook(action);
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs::create_dir_all;
use std::path::PathBuf;
use tempfile::TempDir;
use super::*;
use crate::test_utils::*;
fn setup_sysfs(root_dir: &TempDir) -> (PathBuf, PathBuf, PathBuf) {
let root_path = root_dir.path().to_path_buf();
let root_cpus_path = root_path.join(ROOT_CPUSET_CPUS_PATH).to_path_buf();
let root_cpus_name = &root_cpus_path.display().to_string();
let isolated_cpus_path = root_path.join(ISOLATED_CPUSET_PATH).to_path_buf();
let isolated_cpus_name = &isolated_cpus_path.display().to_string();
create_dir_all(root_cpus_path.parent().unwrap()).expect(root_cpus_name);
create_dir_all(isolated_cpus_path.parent().unwrap()).expect(isolated_cpus_name);
std::fs::write(&isolated_cpus_path, "").expect(isolated_cpus_name);
(root_path, root_cpus_path, isolated_cpus_path)
}
#[test]
fn test_cpuset_all_cores() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
// root cpuset cgroup is not found.
assert!(Cpuset::all_cores(&root_path).is_err());
std::fs::write(&root_cpus_path, "1,2,3,100")
.unwrap_or_else(|_| panic!("{}", root_cpus_path.display().to_string()));
assert_eq!(
Cpuset::all_cores(&root_path).unwrap(),
Cpuset(vec![1, 2, 3, 100])
);
std::fs::write(&root_cpus_path, "100").unwrap();
assert_eq!(Cpuset::all_cores(&root_path).unwrap(), Cpuset(vec![100]));
std::fs::write(&root_cpus_path, "2-10").unwrap();
assert_eq!(
Cpuset::all_cores(&root_path).unwrap(),
Cpuset(vec![2, 3, 4, 5, 6, 7, 8, 9, 10])
);
std::fs::write(&root_cpus_path, "2-2").unwrap();
assert_eq!(Cpuset::all_cores(&root_path).unwrap(), Cpuset(vec![2]));
std::fs::write(&root_cpus_path, "2-2\n").unwrap();
assert_eq!(Cpuset::all_cores(&root_path).unwrap(), Cpuset(vec![2]));
std::fs::write(&root_cpus_path, "").unwrap();
assert_eq!(Cpuset::all_cores(&root_path).unwrap(), Cpuset(vec![]));
std::fs::write(&root_cpus_path, "\n").unwrap();
assert_eq!(Cpuset::all_cores(&root_path).unwrap(), Cpuset(vec![]));
}
#[test]
#[should_panic]
fn test_cpuset_bad() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "a").unwrap();
let _ = Cpuset::all_cores(&root_path).unwrap();
}
#[test]
#[should_panic]
fn test_cpuset_bad2() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, ",").unwrap();
let _ = Cpuset::all_cores(&root_path).unwrap();
}
#[test]
#[should_panic]
fn test_cpuset_bad3() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "1,9-8").unwrap();
let _ = Cpuset::all_cores(&root_path).unwrap();
}
#[test]
#[should_panic]
fn test_cpuset_bad4() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "1,2,1").unwrap();
let _ = Cpuset::all_cores(&root_path).unwrap();
}
fn create_cpus_property(root: &Path, property: &str, values: &[u64]) {
for (i, v) in values.iter().enumerate() {
let property_path = root.join(format!("sys/bus/cpu/devices/cpu{i}/{property}"));
create_dir_all(property_path.parent().unwrap()).unwrap();
std::fs::write(property_path, format!("{v}")).unwrap();
}
}
#[test]
fn test_cpuset_little_cores() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "0-3").unwrap();
// Even if property files are not found, fallbacks to little cores.
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![0, 1, 2, 3])
);
create_cpus_property(&root_path, "cpu_capacity", &[10, 10, 10, 10]);
create_cpus_property(&root_path, "cpufreq/cpuinfo_max_freq", &[20, 20, 20, 20]);
create_cpus_property(&root_path, "acpi_cppc/highest_perf", &[30, 30, 30, 30]);
// Fallback to all cores
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![0, 1, 2, 3])
);
create_cpus_property(&root_path, "acpi_cppc/highest_perf", &[10, 10, 30, 30]);
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![0, 1])
);
create_cpus_property(&root_path, "cpufreq/cpuinfo_max_freq", &[20, 20, 10, 10]);
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![2, 3])
);
create_cpus_property(&root_path, "cpu_capacity", &[1, 10, 1, 10]);
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![0, 2])
);
// Pick the two smallest attribute values.
create_cpus_property(&root_path, "cpu_capacity", &[1, 2, 10, 2]);
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![0, 1, 3])
);
create_cpus_property(&root_path, "cpu_capacity", &[4, 2, 10, 1]);
assert_eq!(
Cpuset::little_cores(&root_path).unwrap(),
Cpuset(vec![1, 3])
);
}
#[test]
fn test_cpuset_little_cores_less_properties() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, _) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "0-3").unwrap();
create_cpus_property(&root_path, "cpufreq/cpuinfo_max_freq", &[20, 10]);
// Skips cores without the property file.
assert_eq!(Cpuset::little_cores(&root_path).unwrap(), Cpuset(vec![1]));
}
#[test]
fn test_cpuset_isolated_cores() {
let root_dir = TempDir::new().unwrap();
let (root_path, root_cpus_path, isolated_path) = setup_sysfs(&root_dir);
std::fs::write(root_cpus_path, "0-15").unwrap();
std::fs::write(isolated_path, "2-4,9").unwrap();
let all_cores = Cpuset::all_cores(&root_path).unwrap();
assert_eq!(all_cores.to_string(), "0-1,5-8,10-15");
}
#[test]
fn test_cpuset_to_string() {
assert_eq!(&Cpuset(vec![]).to_string(), "");
assert_eq!(&Cpuset(vec![1, 2]).to_string(), "1-2");
assert_eq!(&Cpuset(vec![1, 2, 3]).to_string(), "1-3");
assert_eq!(&Cpuset(vec![1, 2, 3, 100]).to_string(), "1-3,100");
assert_eq!(
&Cpuset(vec![1, 2, 3, 5, 6, 7, 9, 99, 100]).to_string(),
"1-3,5-7,9,99-100"
);
assert_eq!(&Cpuset(vec![100]).to_string(), "100");
}
#[test]
fn test_hotplug_cpus() {
const ONLINE: &str = "1";
const OFFLINE: &str = "0";
struct Test<'a> {
cpus: &'a str,
cluster1_freq: [u32; 2],
cluster2_freq: [u32; 2],
hotplug: &'a HotplugCpuAction,
smt: &'a str,
cluster1_expected_state: [&'a str; 2],
cluster2_expected_state: [&'a str; 2],
smt_expected_state: &'a str,
}
let tests = [
// Test offline small cores
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [1800000; 2],
hotplug: &HotplugCpuAction::OfflineSmallCore {
min_active_threads: 2,
},
smt: "on",
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [OFFLINE; 2],
smt_expected_state: "",
},
// Test offline small cores
// No CPU offline when big cores less than min-active-theads
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [1800000; 2],
hotplug: &HotplugCpuAction::OfflineSmallCore {
min_active_threads: 3,
},
smt: "on",
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [ONLINE; 2],
smt_expected_state: "",
},
// Test offline half cores
// Offline half when min-active-theads equals half cores
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [2400000; 2],
hotplug: &HotplugCpuAction::OfflineHalf {
min_active_threads: 2,
},
smt: "on",
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [OFFLINE; 2],
smt_expected_state: "",
},
// Test offline half cores
// CPU offline starts from min-active-threads when
// min-active-theads greater than half cores
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [2400000; 2],
hotplug: &HotplugCpuAction::OfflineHalf {
min_active_threads: 3,
},
smt: "on",
// Expect 3 cores online
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [ONLINE, OFFLINE],
smt_expected_state: "",
},
// Test offline half cores
// No CPU offline when min-active-theads less than all cores
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [2400000; 2],
hotplug: &HotplugCpuAction::OfflineHalf {
min_active_threads: 5,
},
smt: "on",
// Expect 3 cores online
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [ONLINE; 2],
smt_expected_state: "",
},
// Test offline SMT
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [2400000; 2],
hotplug: &HotplugCpuAction::OfflineSMT {
min_active_threads: 2,
},
smt: "on",
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [OFFLINE; 2],
smt_expected_state: "off",
},
// Test offline SMT
// No CPU offline when physical cores less than min-active-theads
Test {
cpus: "0-3",
cluster1_freq: [2400000; 2],
cluster2_freq: [2400000; 2],
hotplug: &HotplugCpuAction::OfflineSMT {
min_active_threads: 3,
},
smt: "on",
cluster1_expected_state: [ONLINE; 2],
cluster2_expected_state: [ONLINE; 2],
smt_expected_state: "on",
},
];
for test in tests {
// Setup.
let root = TempDir::new().unwrap();
test_write_cpuset_root_cpus(root.path(), test.cpus);
test_write_smt_control(root.path(), test.smt);
for (i, freq) in test.cluster1_freq.iter().enumerate() {
test_write_online_cpu(root.path(), i.try_into().unwrap(), "1");
test_write_cpu_max_freq(root.path(), i.try_into().unwrap(), *freq);
}
for (i, freq) in test.cluster2_freq.iter().enumerate() {
test_write_online_cpu(
root.path(),
(test.cluster1_freq.len() + i).try_into().unwrap(),
"1",
);
test_write_cpu_max_freq(
root.path(),
(test.cluster1_freq.len() + i).try_into().unwrap(),
*freq,
);
}
// Setup core cpus list for two physical cores and two virtual cores
test_write_core_cpus_list(root.path(), 0, "0,2");
test_write_core_cpus_list(root.path(), 1, "1,3");
test_write_core_cpus_list(root.path(), 2, "0,2");
test_write_core_cpus_list(root.path(), 3, "1,3");
// Call function to test.
hotplug_cpus(root.path(), *test.hotplug).unwrap();
// Check result.
if let HotplugCpuAction::OfflineSMT {
min_active_threads: _,
} = test.hotplug
{
// The mock sysfs cannot offline the SMT CPUs, here to check the smt control state
test_check_smt_control(root.path(), test.smt_expected_state);
continue;
}
for (i, state) in test.cluster1_expected_state.iter().enumerate() {
test_check_online_cpu(root.path(), i.try_into().unwrap(), state);
}
for (i, state) in test.cluster2_expected_state.iter().enumerate() {
test_check_online_cpu(
root.path(),
(test.cluster1_expected_state.len() + i).try_into().unwrap(),
state,
);
}
}
}
#[test]
fn test_parse_smt_control_status() {
assert_eq!(
SmtControlStatus::from_str("on\n").unwrap(),
SmtControlStatus::On
);
assert_eq!(
SmtControlStatus::from_str("off\n").unwrap(),
SmtControlStatus::Off
);
assert_eq!(
SmtControlStatus::from_str("forceoff\n").unwrap(),
SmtControlStatus::ForceOff
);
assert_eq!(
SmtControlStatus::from_str("notsupported\n").unwrap(),
SmtControlStatus::NotSupported
);
assert_eq!(
SmtControlStatus::from_str("notimplemented\n").unwrap(),
SmtControlStatus::NotImplemented
);
assert!(SmtControlStatus::from_str("").is_err());
assert!(SmtControlStatus::from_str("abc").is_err());
}
#[test]
fn test_is_smt_controllable() {
let root = TempDir::new().unwrap();
let tests = [
("on", true),
("off", true),
("forceoff", false),
("notsupported", false),
("notimplemented", false),
];
for test in tests {
test_write_smt_control(root.path(), test.0);
let result = is_smt_contrallable(root.path()).unwrap();
assert_eq!(result, test.1);
}
}
#[test]
fn test_update_cpu_smt_control() {
let root = TempDir::new().unwrap();
let tests = [
("on", true, "on"),
("on", false, "off"),
("off", true, "on"),
("off", false, "off"),
("forceoff", true, "forceoff"),
("notsupported", true, "notsupported"),
("notimplemented", true, "notimplemented"),
];
for test in tests {
test_write_smt_control(root.path(), test.0);
let _ = update_cpu_smt_control(root.path(), test.1);
test_check_smt_control(root.path(), test.2);
}
}
#[test]
fn test_online_cpus() {
let root_dir = tempfile::tempdir().unwrap();
let root_path = root_dir.path();
let cpu_online_path = root_path.join(CPU_ONLINE_PATH);
create_dir_all(cpu_online_path.parent().unwrap()).unwrap();
std::fs::write(&cpu_online_path, "0-3\n").unwrap();
let cpus = Cpuset::online_cpus(root_path).unwrap();
assert_eq!(cpus.0, vec![0, 1, 2, 3]);
std::fs::write(&cpu_online_path, "0,3-5\n").unwrap();
let cpus = Cpuset::online_cpus(root_path).unwrap();
assert_eq!(cpus.0, vec![0, 3, 4, 5]);
std::fs::write(&cpu_online_path, "0-1,3-5\n").unwrap();
let cpus = Cpuset::online_cpus(root_path).unwrap();
assert_eq!(cpus.0, vec![0, 1, 3, 4, 5]);
}
}