pub mod affinity;
pub mod basic;
mod catalog;
pub mod cpuset;
pub mod dynamic;
pub mod interaction;
pub mod nested;
pub mod ops;
pub mod performance;
pub mod scenarios;
pub mod stress;
pub use catalog::all_scenarios;
use std::collections::BTreeSet;
use std::thread;
use std::time::Duration;
use anyhow::Result;
use nix::sys::signal::kill;
use nix::unistd::Pid;
use crate::assert::{self, AssertResult};
use crate::cgroup::CgroupManager;
use crate::topology::TestTopology;
use crate::workload::*;
fn process_alive(pid: u32) -> bool {
kill(Pid::from_raw(pid as i32), None).is_ok()
}
pub(crate) use crate::read_kmsg;
pub mod flags {
pub struct FlagDecl {
pub name: &'static str,
pub args: &'static [&'static str],
pub requires: &'static [&'static FlagDecl],
}
impl std::fmt::Debug for FlagDecl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let req_names: Vec<&str> = self.requires.iter().map(|d| d.name).collect();
f.debug_struct("FlagDecl")
.field("name", &self.name)
.field("args", &self.args)
.field("requires", &req_names)
.finish()
}
}
pub static LLC_DECL: FlagDecl = FlagDecl {
name: "llc",
args: &[],
requires: &[],
};
pub static BORROW_DECL: FlagDecl = FlagDecl {
name: "borrow",
args: &[],
requires: &[],
};
pub static STEAL_DECL: FlagDecl = FlagDecl {
name: "steal",
args: &[],
requires: &[&LLC_DECL],
};
pub static REBAL_DECL: FlagDecl = FlagDecl {
name: "rebal",
args: &[],
requires: &[],
};
pub static REJECT_PIN_DECL: FlagDecl = FlagDecl {
name: "reject-pin",
args: &[],
requires: &[],
};
pub static NO_CTRL_DECL: FlagDecl = FlagDecl {
name: "no-ctrl",
args: &[],
requires: &[],
};
pub static ALL_DECLS: &[&FlagDecl] = &[
&LLC_DECL,
&BORROW_DECL,
&STEAL_DECL,
&REBAL_DECL,
&REJECT_PIN_DECL,
&NO_CTRL_DECL,
];
pub const LLC: &str = "llc";
pub const BORROW: &str = "borrow";
pub const STEAL: &str = "steal";
pub const REBAL: &str = "rebal";
pub const REJECT_PIN: &str = "reject-pin";
pub const NO_CTRL: &str = "no-ctrl";
pub const ALL: &[&str] = &[LLC, BORROW, STEAL, REBAL, REJECT_PIN, NO_CTRL];
pub fn from_short_name(s: &str) -> Option<&'static str> {
ALL.iter().find(|&&f| f == s).copied()
}
pub fn decl_by_name(name: &str) -> Option<&'static FlagDecl> {
ALL_DECLS.iter().find(|d| d.name == name).copied()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FlagDeclJson {
pub name: String,
pub args: Vec<String>,
pub requires: Vec<String>,
}
impl FlagDeclJson {
pub fn from_decl(decl: &FlagDecl) -> Self {
Self {
name: decl.name.to_string(),
args: decl.args.iter().map(|s| s.to_string()).collect(),
requires: decl.requires.iter().map(|r| r.name.to_string()).collect(),
}
}
}
}
#[derive(Debug, Clone)]
pub struct FlagProfile {
pub flags: Vec<&'static str>,
}
impl FlagProfile {
pub fn name(&self) -> String {
if self.flags.is_empty() {
"default".into()
} else {
self.flags.join("+")
}
}
}
pub use crate::workload::AffinityKind;
fn flag_requires(flag: &str) -> Vec<&'static str> {
flags::decl_by_name(flag)
.map(|d| d.requires.iter().map(|r| r.name).collect())
.unwrap_or_default()
}
fn generate_profiles(required: &[&'static str], excluded: &[&'static str]) -> Vec<FlagProfile> {
let optional: Vec<&'static str> = flags::ALL
.iter()
.copied()
.filter(|f| !required.contains(f) && !excluded.contains(f))
.collect();
let mut out = Vec::new();
for mask in 0..(1u32 << optional.len()) {
let mut fl: Vec<&'static str> = required.to_vec();
for (i, &f) in optional.iter().enumerate() {
if mask & (1 << i) != 0 {
fl.push(f);
}
}
let valid = fl
.iter()
.all(|f| flag_requires(f).iter().all(|r| fl.contains(r)));
if valid {
fl.sort_by_key(|f| flags::ALL.iter().position(|a| a == f).unwrap_or(usize::MAX));
out.push(FlagProfile { flags: fl });
}
}
out
}
#[derive(Clone, Debug)]
pub enum CpusetMode {
None,
LlcAligned,
SplitHalf,
SplitMisaligned,
Overlap(f64),
Uneven(f64), Holdback(f64), }
#[derive(Clone)]
pub enum Action {
Steady,
Custom(fn(&Ctx) -> Result<AssertResult>),
}
impl std::fmt::Debug for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Steady => write!(f, "Steady"),
Action::Custom(_) => write!(f, "Custom(fn)"),
}
}
}
#[derive(Clone, Debug)]
pub struct CgroupWork {
pub num_workers: Option<usize>,
pub work_type: WorkType,
pub policy: SchedPolicy,
pub affinity: AffinityKind,
}
impl Default for CgroupWork {
fn default() -> Self {
Self {
num_workers: None,
work_type: WorkType::CpuSpin,
policy: SchedPolicy::Normal,
affinity: AffinityKind::Inherit,
}
}
}
#[derive(Clone, Debug)]
pub struct Scenario {
pub name: &'static str,
pub category: &'static str,
pub description: &'static str,
pub required_flags: &'static [&'static flags::FlagDecl],
pub excluded_flags: &'static [&'static flags::FlagDecl],
pub num_cgroups: usize,
pub cpuset_mode: CpusetMode,
pub cgroup_works: Vec<CgroupWork>,
pub action: Action,
}
impl Scenario {
pub fn profiles(&self) -> Vec<FlagProfile> {
let req: Vec<&'static str> = self.required_flags.iter().map(|d| d.name).collect();
let excl: Vec<&'static str> = self.excluded_flags.iter().map(|d| d.name).collect();
generate_profiles(&req, &excl)
}
#[allow(dead_code)]
pub fn profiles_with(&self, active: &[&str]) -> Vec<FlagProfile> {
let req: Vec<&'static str> = self.required_flags.iter().map(|d| d.name).collect();
let mut excl: Vec<&'static str> = self.excluded_flags.iter().map(|d| d.name).collect();
for &f in flags::ALL {
if !active.contains(&f) && !req.contains(&f) {
excl.push(f);
}
}
generate_profiles(&req, &excl)
}
pub fn qualified_name(&self, p: &FlagProfile) -> String {
format!("{}/{}", self.name, p.name())
}
}
#[derive(Debug)]
#[must_use = "dropping a CgroupGroup immediately destroys the cgroups it manages"]
pub struct CgroupGroup<'a> {
cgroups: &'a CgroupManager,
names: Vec<String>,
}
impl<'a> CgroupGroup<'a> {
pub fn new(cgroups: &'a CgroupManager) -> Self {
Self {
cgroups,
names: Vec::new(),
}
}
pub fn add_cgroup(&mut self, name: &str, cpuset: &BTreeSet<usize>) -> Result<()> {
self.cgroups.create_cgroup(name)?;
self.cgroups.set_cpuset(name, cpuset)?;
self.names.push(name.to_string());
Ok(())
}
pub fn add_cgroup_no_cpuset(&mut self, name: &str) -> Result<()> {
self.cgroups.create_cgroup(name)?;
self.names.push(name.to_string());
Ok(())
}
pub fn names(&self) -> &[String] {
&self.names
}
}
impl Drop for CgroupGroup<'_> {
fn drop(&mut self) {
for name in &self.names {
let _ = self.cgroups.remove_cgroup(name);
}
}
}
#[derive(Debug)]
pub struct Ctx<'a> {
pub cgroups: &'a CgroupManager,
pub topo: &'a TestTopology,
pub duration: Duration,
pub workers_per_cgroup: usize,
pub sched_pid: u32,
pub settle: Duration,
pub work_type_override: Option<WorkType>,
pub assert: crate::assert::Assert,
pub wait_for_map_write: bool,
}
pub fn run_scenario(scenario: &Scenario, ctx: &Ctx) -> Result<AssertResult> {
tracing::info!(scenario = scenario.name, "running");
if let Action::Custom(f) = &scenario.action {
return f(ctx);
}
let cpusets = resolve_cpusets(&scenario.cpuset_mode, scenario.num_cgroups, ctx.topo);
if let Some(ref cs) = cpusets
&& cs.iter().any(|s| s.is_empty())
{
return Ok(AssertResult::skip("skipped: not enough CPUs/LLCs"));
}
let scenario_start = std::time::Instant::now();
let names: Vec<String> = (0..scenario.num_cgroups)
.map(|i| format!("cg_{i}"))
.collect();
let mut cgroup_guard = CgroupGroup::new(ctx.cgroups);
for (i, name) in names.iter().enumerate() {
cgroup_guard.add_cgroup_no_cpuset(name)?;
if let Some(ref cs) = cpusets {
ctx.cgroups.set_cpuset(name, &cs[i])?;
}
}
tracing::debug!(cgroups = scenario.num_cgroups, "cgroups created, settling");
thread::sleep(ctx.settle);
if !process_alive(ctx.sched_pid) {
anyhow::bail!("scheduler died after cgroup creation");
}
let mut handles = Vec::new();
for (i, name) in names.iter().enumerate() {
let cw = scenario
.cgroup_works
.get(i)
.or(scenario.cgroup_works.first())
.cloned()
.unwrap_or_default();
let n = cw.num_workers.unwrap_or(ctx.workers_per_cgroup);
let affinity = resolve_affinity_kind(&cw.affinity, cpusets.as_deref(), i, ctx.topo);
let effective_work_type = crate::workload::resolve_work_type(
&cw.work_type,
ctx.work_type_override.as_ref(),
matches!(cw.work_type, WorkType::CpuSpin),
n,
);
let wl = WorkloadConfig {
num_workers: n,
affinity,
work_type: effective_work_type,
sched_policy: cw.policy,
};
let h = WorkloadHandle::spawn(&wl)?;
tracing::debug!(cgroup = %name, workers = n, tids = h.tids().len(), "spawned workers");
for tid in h.tids() {
ctx.cgroups.move_task(name, tid)?;
}
handles.push(h);
}
for h in &mut handles {
h.start();
}
tracing::debug!(duration_s = ctx.duration.as_secs(), "running workload");
let deadline = std::time::Instant::now() + ctx.duration;
let mut sched_dead = false;
while std::time::Instant::now() < deadline {
if !process_alive(ctx.sched_pid) {
sched_dead = true;
tracing::warn!("scheduler died during workload phase");
break;
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
thread::sleep(remaining.min(Duration::from_millis(500)));
}
if !sched_dead {
sched_dead = !process_alive(ctx.sched_pid);
}
let mut result = AssertResult::pass();
for (i, h) in handles.into_iter().enumerate() {
let reports = h.stop_and_collect();
let cs = cpusets.as_ref().map(|v| &v[i]);
result.merge(ctx.assert.assert_cgroup(&reports, cs));
}
if !result.passed {
for line in read_kmsg().lines() {
result.details.push(line.to_string());
}
}
if sched_dead {
result.passed = false;
result.details.push(format!(
"scheduler crashed during workload ({:.1}s into test)",
scenario_start.elapsed().as_secs_f64(),
));
}
Ok(result)
}
fn resolve_cpusets(
mode: &CpusetMode,
n: usize,
topo: &TestTopology,
) -> Option<Vec<BTreeSet<usize>>> {
let all = topo.all_cpus();
let usable = topo.usable_cpus();
match mode {
CpusetMode::None => None,
CpusetMode::LlcAligned => {
let llcs = topo.split_by_llc();
if llcs.len() < 2 {
return Some(vec![BTreeSet::new()]);
}
let mut sets: Vec<BTreeSet<usize>> = llcs[..n.min(llcs.len())].to_vec();
if let Some(last) = sets.last_mut()
&& last.len() > 1
{
last.remove(&all[all.len() - 1]);
}
Some(sets)
}
CpusetMode::SplitHalf => {
let mid = usable.len() / 2;
Some(vec![
usable[..mid].iter().copied().collect(),
usable[mid..].iter().copied().collect(),
])
}
CpusetMode::SplitMisaligned => {
let split = if topo.num_llcs() > 1 {
topo.cpus_in_llc(0).len() / 2
} else {
usable.len() / 2
};
Some(vec![
usable[..split].iter().copied().collect(),
usable[split..].iter().copied().collect(),
])
}
CpusetMode::Overlap(frac) => Some(topo.overlapping_cpusets(n, *frac)),
CpusetMode::Uneven(frac) => {
let split = (usable.len() as f64 * frac) as usize;
Some(vec![
usable[..split.max(1)].iter().copied().collect(),
usable[split.max(1)..].iter().copied().collect(),
])
}
CpusetMode::Holdback(frac) => {
let keep = all.len() - (all.len() as f64 * frac) as usize;
let mid = keep / 2;
Some(vec![
all[..mid.max(1)].iter().copied().collect(),
all[mid.max(1)..keep].iter().copied().collect(),
])
}
}
}
pub fn resolve_affinity_for_cgroup(
kind: &AffinityKind,
cpuset: Option<&BTreeSet<usize>>,
topo: &TestTopology,
) -> AffinityMode {
match kind {
AffinityKind::Inherit => AffinityMode::None,
AffinityKind::RandomSubset => {
let pool = cpuset.cloned().unwrap_or_else(|| topo.all_cpuset());
let count = (pool.len() / 2).max(1);
AffinityMode::Random { from: pool, count }
}
AffinityKind::LlcAligned => {
let pool = cpuset.cloned().unwrap_or_else(|| topo.all_cpuset());
let mut best_llc = topo.llc_aligned_cpuset(0);
let mut best_overlap = best_llc.intersection(&pool).count();
for idx in 1..topo.num_llcs() {
let llc = topo.llc_aligned_cpuset(idx);
let overlap = llc.intersection(&pool).count();
if overlap > best_overlap {
best_llc = llc;
best_overlap = overlap;
}
}
let effective: BTreeSet<usize> = best_llc.intersection(&pool).copied().collect();
if effective.is_empty() {
AffinityMode::None
} else {
AffinityMode::Fixed(effective)
}
}
AffinityKind::CrossCgroup => {
AffinityMode::Fixed(topo.all_cpuset())
}
AffinityKind::SingleCpu => {
let pool = cpuset.cloned().unwrap_or_else(|| topo.all_cpuset());
if let Some(&cpu) = pool.iter().next() {
AffinityMode::SingleCpu(cpu)
} else {
AffinityMode::None
}
}
AffinityKind::Exact(cpus) => {
if let Some(cs) = cpuset {
let effective: BTreeSet<usize> = cpus.intersection(cs).copied().collect();
if effective.is_empty() {
AffinityMode::None
} else {
AffinityMode::Fixed(effective)
}
} else {
AffinityMode::Fixed(cpus.clone())
}
}
}
}
fn resolve_affinity_kind(
kind: &AffinityKind,
cpusets: Option<&[BTreeSet<usize>]>,
cgroup_idx: usize,
topo: &TestTopology,
) -> AffinityMode {
let cpuset = cpusets.map(|cs| &cs[cgroup_idx]);
resolve_affinity_for_cgroup(kind, cpuset, topo)
}
pub fn setup_cgroups<'a>(
ctx: &'a Ctx,
n: usize,
wl: &WorkloadConfig,
) -> Result<(Vec<WorkloadHandle>, CgroupGroup<'a>)> {
let mut guard = CgroupGroup::new(ctx.cgroups);
for i in 0..n {
guard.add_cgroup_no_cpuset(&format!("cg_{i}"))?;
}
thread::sleep(ctx.settle);
if !process_alive(ctx.sched_pid) {
anyhow::bail!("scheduler died after cgroup creation");
}
let handles: Result<Vec<_>> = (0..n)
.map(|i| {
let h = WorkloadHandle::spawn(wl)?;
ctx.cgroups.move_tasks(&format!("cg_{i}"), &h.tids())?;
Ok(h)
})
.collect();
let mut handles = handles?;
for h in &mut handles {
h.start();
}
Ok((handles, guard))
}
pub(crate) fn collect_handles<'a>(
handles: impl IntoIterator<Item = (WorkloadHandle, Option<&'a BTreeSet<usize>>)>,
checks: &crate::assert::Assert,
) -> AssertResult {
let mut r = AssertResult::pass();
for (h, cpuset) in handles {
let reports = h.stop_and_collect();
if checks.has_worker_checks() {
r.merge(checks.assert_cgroup(&reports, cpuset));
} else {
r.merge(assert::assert_not_starved(&reports));
}
}
r
}
pub fn collect_all(handles: Vec<WorkloadHandle>, checks: &crate::assert::Assert) -> AssertResult {
collect_handles(handles.into_iter().map(|h| (h, None)), checks)
}
pub fn dfl_wl(ctx: &Ctx) -> WorkloadConfig {
WorkloadConfig {
num_workers: ctx.workers_per_cgroup,
..Default::default()
}
}
#[cfg(test)]
pub fn split_half(ctx: &Ctx) -> (BTreeSet<usize>, BTreeSet<usize>) {
let usable = ctx.topo.usable_cpus();
let mid = usable.len() / 2;
(
usable[..mid].iter().copied().collect(),
usable[mid..].iter().copied().collect(),
)
}
pub fn spawn_diverse(ctx: &Ctx, cgroup_names: &[&str]) -> Result<Vec<WorkloadHandle>> {
let types = [
WorkType::CpuSpin,
WorkType::bursty(50, 100),
WorkType::IoSync,
WorkType::Mixed,
WorkType::YieldHeavy,
];
let mut handles = Vec::new();
for (i, name) in cgroup_names.iter().enumerate() {
let wt = types[i % types.len()].clone();
let n = if matches!(wt, WorkType::IoSync) {
2
} else {
ctx.workers_per_cgroup
};
let mut h = WorkloadHandle::spawn(&WorkloadConfig {
num_workers: n,
work_type: wt,
..Default::default()
})?;
ctx.cgroups.move_tasks(name, &h.tids())?;
h.start();
handles.push(h);
}
Ok(handles)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_short_name_roundtrip() {
for &f in flags::ALL {
assert_eq!(flags::from_short_name(f), Some(f));
}
}
#[test]
fn flag_all_unique_short_names() {
let unique: std::collections::HashSet<&&str> = flags::ALL.iter().collect();
assert_eq!(flags::ALL.len(), unique.len());
}
#[test]
fn flag_from_short_name_unknown() {
assert_eq!(flags::from_short_name("nonexistent"), None);
}
#[test]
fn profile_name_default() {
assert_eq!(FlagProfile { flags: vec![] }.name(), "default");
}
#[test]
fn profile_name_with_flags() {
let p = FlagProfile {
flags: vec![flags::LLC, flags::BORROW],
};
assert_eq!(p.name(), "llc+borrow");
}
#[test]
fn generate_profiles_no_constraints() {
assert_eq!(generate_profiles(&[], &[]).len(), 48);
}
#[test]
fn generate_profiles_work_stealing_requires_llc() {
let profiles = generate_profiles(&[flags::STEAL], &[]);
for p in &profiles {
assert!(
p.flags.contains(&flags::LLC),
"steal without llc: {:?}",
p.flags
);
}
}
#[test]
fn generate_profiles_excluded_never_present() {
let profiles = generate_profiles(&[], &[flags::NO_CTRL]);
for p in &profiles {
assert!(!p.flags.contains(&flags::NO_CTRL));
}
}
#[test]
fn generate_profiles_required_always_present() {
let profiles = generate_profiles(&[flags::BORROW], &[]);
for p in &profiles {
assert!(p.flags.contains(&flags::BORROW));
}
}
#[test]
fn generate_profiles_required_and_excluded() {
let profiles = generate_profiles(&[flags::BORROW], &[flags::REBAL]);
for p in &profiles {
assert!(p.flags.contains(&flags::BORROW));
assert!(!p.flags.contains(&flags::REBAL));
}
}
#[test]
fn all_scenarios_non_empty() {
assert!(!all_scenarios().is_empty());
}
#[test]
fn all_scenarios_unique_names() {
let scenarios = all_scenarios();
let names: Vec<&str> = scenarios.iter().map(|s| s.name).collect();
let unique: std::collections::HashSet<&&str> = names.iter().collect();
assert_eq!(names.len(), unique.len(), "duplicate scenario names");
}
#[test]
fn all_scenarios_have_profiles() {
for s in &all_scenarios() {
assert!(!s.profiles().is_empty(), "{} has no valid profiles", s.name);
}
}
#[test]
fn resolve_cpusets_none_returns_none() {
let t = crate::topology::TestTopology::synthetic(8, 2);
assert!(resolve_cpusets(&CpusetMode::None, 2, &t).is_none());
}
#[test]
fn resolve_cpusets_split_half_covers_usable() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::SplitHalf, 2, &t).unwrap();
assert_eq!(r.len(), 2);
let total: usize = r.iter().map(|s| s.len()).sum();
assert_eq!(total, 7);
}
#[test]
fn resolve_cpusets_llc_aligned() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::LlcAligned, 2, &t).unwrap();
assert_eq!(r.len(), 2);
assert!(!r[0].is_empty());
assert!(!r[1].is_empty());
}
#[test]
fn resolve_cpusets_uneven() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Uneven(0.75), 2, &t).unwrap();
assert!(r[0].len() > r[1].len(), "75/25 split should be uneven");
}
#[test]
fn resolve_cpusets_holdback() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Holdback(0.5), 2, &t).unwrap();
let total: usize = r.iter().map(|s| s.len()).sum();
assert!(total < 8, "holdback should use fewer CPUs");
}
#[test]
fn resolve_cpusets_overlap() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Overlap(0.5), 3, &t).unwrap();
assert_eq!(r.len(), 3);
}
#[test]
fn resolve_affinity_inherit() {
let t = crate::topology::TestTopology::synthetic(8, 2);
assert!(matches!(
resolve_affinity_kind(&AffinityKind::Inherit, None, 0, &t),
AffinityMode::None
));
}
#[test]
fn resolve_affinity_single_cpu() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_kind(&AffinityKind::SingleCpu, None, 0, &t) {
AffinityMode::SingleCpu(c) => assert_eq!(c, 0),
other => panic!("expected SingleCpu, got {:?}", other),
}
}
#[test]
fn resolve_affinity_cross_cgroup() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_kind(&AffinityKind::CrossCgroup, None, 0, &t) {
AffinityMode::Fixed(cpus) => assert_eq!(cpus.len(), 8),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_kind(&AffinityKind::LlcAligned, None, 0, &t) {
AffinityMode::Fixed(cpus) => assert_eq!(cpus, [0, 1, 2, 3].into_iter().collect()),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned_with_cpuset() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![
[0, 1, 2, 3].into_iter().collect(),
[4, 5, 6, 7].into_iter().collect(),
];
match resolve_affinity_kind(&AffinityKind::LlcAligned, Some(&cpusets), 1, &t) {
AffinityMode::Fixed(cpus) => assert_eq!(cpus, [4, 5, 6, 7].into_iter().collect()),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_random_subset() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![[0, 1, 2, 3].into_iter().collect()];
match resolve_affinity_kind(&AffinityKind::RandomSubset, Some(&cpusets), 0, &t) {
AffinityMode::Random { from, count } => {
assert_eq!(from, cpusets[0]);
assert_eq!(count, 2); }
other => panic!("expected Random, got {:?}", other),
}
}
#[test]
fn profiles_with_filters_correctly() {
let s = &all_scenarios()[0]; let profiles = s.profiles_with(&[flags::BORROW]);
for p in &profiles {
for f in &p.flags {
assert!(
*f == flags::BORROW || flag_requires(flags::BORROW).contains(f),
"unexpected flag {:?}",
f
);
}
}
}
#[test]
fn resolve_cpusets_split_misaligned() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::SplitMisaligned, 2, &t).unwrap();
assert_eq!(r.len(), 2);
let total: usize = r.iter().map(|s| s.len()).sum();
assert!(total > 0);
assert_ne!(r[0].len(), 4, "misaligned should NOT split at LLC boundary");
}
#[test]
fn resolve_cpusets_llc_aligned_single_llc() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let r = resolve_cpusets(&CpusetMode::LlcAligned, 2, &t).unwrap();
assert!(
r.iter().any(|s| s.is_empty()),
"should signal skip with empty set"
);
}
#[test]
fn resolve_cpusets_small_topology() {
let t = crate::topology::TestTopology::synthetic(2, 1);
let r = resolve_cpusets(&CpusetMode::SplitHalf, 2, &t).unwrap();
assert_eq!(r.len(), 2);
assert_eq!(r[0].len(), 1);
assert_eq!(r[1].len(), 1);
}
#[test]
fn cgroup_work_default() {
let cw = CgroupWork::default();
assert_eq!(cw.num_workers, None);
assert!(matches!(cw.work_type, WorkType::CpuSpin));
assert!(matches!(cw.policy, SchedPolicy::Normal));
assert!(matches!(cw.affinity, AffinityKind::Inherit));
}
#[test]
fn scenario_qualified_name() {
let s = &all_scenarios()[0];
let p = FlagProfile { flags: vec![] };
assert_eq!(s.qualified_name(&p), format!("{}/default", s.name));
}
#[test]
fn scenario_qualified_name_with_flags() {
let s = &all_scenarios()[0];
let p = FlagProfile {
flags: vec![flags::LLC, flags::BORROW],
};
assert_eq!(s.qualified_name(&p), format!("{}/llc+borrow", s.name));
}
#[test]
fn all_scenarios_count() {
let scenarios = all_scenarios();
assert!(
scenarios.len() >= 30,
"expected >=30 scenarios, got {}",
scenarios.len()
);
}
#[test]
fn scenario_categories_valid() {
let valid = [
"basic",
"cpuset",
"affinity",
"sched_class",
"dynamic",
"stress",
"stall",
"advanced",
"nested",
"interaction",
"performance",
];
for s in &all_scenarios() {
assert!(
valid.contains(&s.category),
"unknown category '{}' in {}",
s.category,
s.name
);
}
}
#[test]
fn generate_profiles_single_required_count() {
assert_eq!(generate_profiles(&[flags::BORROW], &[]).len(), 24);
}
#[test]
fn profiles_sorted_by_flag_order() {
for p in &generate_profiles(&[], &[]) {
for w in p.flags.windows(2) {
let pos0 = flags::ALL.iter().position(|a| a == &w[0]).unwrap();
let pos1 = flags::ALL.iter().position(|a| a == &w[1]).unwrap();
assert!(pos0 < pos1, "flags not sorted: {:?}", p.flags);
}
}
}
#[test]
fn resolve_cpusets_holdback_reserves_cpus() {
let t = crate::topology::TestTopology::synthetic(12, 3);
let r = resolve_cpusets(&CpusetMode::Holdback(0.33), 2, &t).unwrap();
let total: usize = r.iter().map(|s| s.len()).sum();
assert_eq!(total, 9, "holdback 33% of 12 should keep 9");
assert!(total < 12, "holdback should use fewer CPUs than total");
assert_eq!(r.len(), 2);
}
#[test]
fn resolve_cpusets_overlap_sets_overlap() {
let t = crate::topology::TestTopology::synthetic(12, 1);
let r = resolve_cpusets(&CpusetMode::Overlap(0.5), 2, &t).unwrap();
let overlap: BTreeSet<usize> = r[0].intersection(&r[1]).copied().collect();
assert!(
!overlap.is_empty(),
"50% overlap should have overlapping CPUs"
);
}
#[test]
fn resolve_affinity_random_no_cpusets() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_kind(&AffinityKind::RandomSubset, None, 0, &t) {
AffinityMode::Random { from, count } => {
assert_eq!(from.len(), 8); assert_eq!(count, 4); }
other => panic!("expected Random, got {:?}", other),
}
}
#[test]
fn split_half_even() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 4,
sched_pid: 0,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
};
let (a, b) = split_half(&ctx);
assert_eq!(a.len() + b.len(), 7);
assert!(a.intersection(&b).count() == 0, "halves should not overlap");
}
#[test]
fn split_half_small() {
let t = crate::topology::TestTopology::synthetic(2, 1);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 1,
sched_pid: 0,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
};
let (a, b) = split_half(&ctx);
assert_eq!(a.len() + b.len(), 2);
}
#[test]
fn dfl_wl_propagates_workers() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 7,
sched_pid: 0,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
};
let wl = dfl_wl(&ctx);
assert_eq!(wl.num_workers, 7);
assert!(matches!(wl.work_type, WorkType::CpuSpin));
}
#[test]
fn process_alive_current_pid() {
let pid = std::process::id();
assert!(process_alive(pid));
}
#[test]
fn process_alive_nonexistent_pid() {
let pid_max: u32 = std::fs::read_to_string("/proc/sys/kernel/pid_max")
.unwrap_or_else(|_| "4194304".into())
.trim()
.parse()
.unwrap_or(4194304);
if process_alive(pid_max - 1) {
return;
}
assert!(!process_alive(pid_max - 1));
}
#[test]
fn cgroup_group_new_empty() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let group = CgroupGroup::new(&cg);
assert!(group.names().is_empty());
}
#[test]
fn decl_by_name_valid() {
for &name in flags::ALL {
assert!(flags::decl_by_name(name).is_some(), "should find {name}");
}
}
#[test]
fn decl_by_name_unknown() {
assert!(flags::decl_by_name("nonexistent").is_none());
}
#[test]
fn decl_by_name_steal_requires_llc() {
let steal = flags::decl_by_name("steal").unwrap();
assert_eq!(steal.requires.len(), 1);
assert_eq!(steal.requires[0].name, "llc");
}
#[test]
fn decl_by_name_borrow_no_requires() {
let borrow = flags::decl_by_name("borrow").unwrap();
assert!(borrow.requires.is_empty());
}
#[test]
fn flag_requires_steal_returns_llc() {
let req = flag_requires("steal");
assert_eq!(req, vec!["llc"]);
}
#[test]
fn flag_requires_borrow_returns_empty() {
assert!(flag_requires("borrow").is_empty());
}
#[test]
fn flag_requires_unknown_returns_empty() {
assert!(flag_requires("nonexistent").is_empty());
}
#[test]
fn profile_name_three_flags() {
let p = FlagProfile {
flags: vec![flags::LLC, flags::BORROW, flags::REBAL],
};
assert_eq!(p.name(), "llc+borrow+rebal");
}
#[test]
fn resolve_cpusets_split_misaligned_single_llc() {
let t = crate::topology::TestTopology::synthetic(8, 1);
let r = resolve_cpusets(&CpusetMode::SplitMisaligned, 2, &t).unwrap();
assert_eq!(r.len(), 2);
let total: usize = r.iter().map(|s| s.len()).sum();
assert!(total > 0);
}
#[test]
fn resolve_cpusets_uneven_small_frac() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Uneven(0.1), 2, &t).unwrap();
assert!(
r[0].len() < r[1].len(),
"0.1 fraction should give smaller first set"
);
}
#[test]
fn scenario_profiles_count_bounded() {
for s in &all_scenarios() {
let n = s.profiles().len();
assert!(n >= 1, "{} has {} profiles", s.name, n);
assert!(n <= 48, "{} has {} profiles (>48)", s.name, n);
}
}
#[test]
fn all_decls_matches_all_strings() {
assert_eq!(flags::ALL_DECLS.len(), flags::ALL.len());
for (decl, &name) in flags::ALL_DECLS.iter().zip(flags::ALL.iter()) {
assert_eq!(decl.name, name);
}
}
#[test]
fn resolve_affinity_single_cpu_with_cpuset() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let cpusets: Vec<BTreeSet<usize>> = vec![[2, 3].into_iter().collect()];
match resolve_affinity_kind(&AffinityKind::SingleCpu, Some(&cpusets), 0, &t) {
AffinityMode::SingleCpu(c) => assert_eq!(c, 2),
other => panic!("expected SingleCpu, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned_picks_best_overlap() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![[3, 4, 5, 6, 7].into_iter().collect()];
match resolve_affinity_kind(&AffinityKind::LlcAligned, Some(&cpusets), 0, &t) {
AffinityMode::Fixed(cpus) => {
assert_eq!(cpus, [4, 5, 6, 7].into_iter().collect());
}
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn qualified_name_all_scenarios_with_default() {
let p = FlagProfile { flags: vec![] };
for s in &all_scenarios() {
assert_eq!(
s.qualified_name(&p),
format!("{}/default", s.name),
"qualified_name mismatch for {}",
s.name
);
}
}
#[test]
fn qualified_name_single_flag() {
let s = &all_scenarios()[0];
let p = FlagProfile {
flags: vec![flags::REBAL],
};
assert_eq!(s.qualified_name(&p), format!("{}/rebal", s.name));
}
#[test]
fn qualified_name_three_flags_joined() {
let s = &all_scenarios()[0];
let p = FlagProfile {
flags: vec![flags::LLC, flags::STEAL, flags::BORROW],
};
assert_eq!(s.qualified_name(&p), format!("{}/llc+steal+borrow", s.name));
}
#[test]
fn generate_profiles_steal_only_forces_llc() {
let profiles = generate_profiles(&[flags::STEAL], &[]);
assert!(!profiles.is_empty());
for p in &profiles {
assert!(
p.flags.contains(&flags::STEAL),
"steal missing: {:?}",
p.flags
);
assert!(
p.flags.contains(&flags::LLC),
"llc missing when steal present: {:?}",
p.flags
);
}
}
#[test]
fn generate_profiles_all_excluded_returns_single_empty() {
let profiles = generate_profiles(&[], flags::ALL);
assert_eq!(profiles.len(), 1);
assert!(profiles[0].flags.is_empty());
}
#[test]
fn generate_profiles_required_and_excluded_all_others() {
let excluded: Vec<&str> = flags::ALL
.iter()
.copied()
.filter(|f| *f != flags::BORROW)
.collect();
let profiles = generate_profiles(&[flags::BORROW], &excluded);
assert_eq!(profiles.len(), 1);
assert_eq!(profiles[0].flags, vec![flags::BORROW]);
}
#[test]
fn resolve_cpusets_split_half_exact_cpu_assignment() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::SplitHalf, 2, &t).unwrap();
assert_eq!(r[0], [0, 1, 2].into_iter().collect());
assert_eq!(r[1], [3, 4, 5, 6].into_iter().collect());
}
#[test]
fn resolve_cpusets_uneven_75_exact_split() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Uneven(0.75), 2, &t).unwrap();
assert_eq!(r[0].len(), 5);
assert_eq!(r[1].len(), 2);
assert_eq!(r[0], [0, 1, 2, 3, 4].into_iter().collect());
assert_eq!(r[1], [5, 6].into_iter().collect());
}
#[test]
fn resolve_cpusets_holdback_50_exact() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let r = resolve_cpusets(&CpusetMode::Holdback(0.5), 2, &t).unwrap();
let total: usize = r.iter().map(|s| s.len()).sum();
assert_eq!(total, 4, "holdback 50% of 8 should keep 4");
assert_eq!(r[0], [0, 1].into_iter().collect());
assert_eq!(r[1], [2, 3].into_iter().collect());
}
}