use std::borrow::Cow;
use std::collections::BTreeSet;
use std::time::Duration;
use super::{AffinityIntent, WorkType};
pub(crate) mod humantime_serde_helper {
use std::time::Duration;
pub fn serialize<S: serde::Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&humantime::format_duration(*d).to_string())
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
let s = <String as serde::Deserialize>::deserialize(d)?;
humantime::parse_duration(&s).map_err(serde::de::Error::custom)
}
}
pub mod defaults {
pub const BURSTY_BURST_DURATION: std::time::Duration = std::time::Duration::from_millis(50);
pub const BURSTY_SLEEP_DURATION: std::time::Duration = std::time::Duration::from_millis(100);
pub const PIPE_IO_BURST_ITERS: u64 = 1024;
pub const FUTEX_PING_PONG_SPIN_ITERS: u64 = 1024;
pub const CACHE_PRESSURE_SIZE_KB: usize = 32;
pub const CACHE_PRESSURE_STRIDE: usize = 64;
pub const CACHE_YIELD_SIZE_KB: usize = 32;
pub const CACHE_YIELD_STRIDE: usize = 64;
pub const CACHE_PIPE_SIZE_KB: usize = 32;
pub const CACHE_PIPE_BURST_ITERS: u64 = 1024;
pub const FUTEX_FAN_OUT_FAN_OUT: usize = 4;
pub const FUTEX_FAN_OUT_SPIN_ITERS: u64 = 1024;
pub const AFFINITY_CHURN_SPIN_ITERS: u64 = 1024;
pub const POLICY_CHURN_SPIN_ITERS: u64 = 1024;
pub const FAN_OUT_COMPUTE_FAN_OUT: usize = 4;
pub const FAN_OUT_COMPUTE_CACHE_FOOTPRINT_KB: usize = 256;
pub const FAN_OUT_COMPUTE_OPERATIONS: usize = 5;
pub const FAN_OUT_COMPUTE_SLEEP_USEC: u64 = 100;
pub const PAGE_FAULT_CHURN_REGION_KB: usize = 4096;
pub const PAGE_FAULT_CHURN_TOUCHES_PER_CYCLE: usize = 256;
pub const PAGE_FAULT_CHURN_SPIN_ITERS: u64 = 64;
pub const MUTEX_CONTENTION_CONTENDERS: usize = 4;
pub const MUTEX_CONTENTION_HOLD_ITERS: u64 = 256;
pub const MUTEX_CONTENTION_WORK_ITERS: u64 = 1024;
pub const THUNDERING_HERD_WAITERS: usize = 7;
pub const THUNDERING_HERD_BATCHES: u64 = 1_000;
pub const THUNDERING_HERD_INTER_BATCH_MS: u64 = 5;
pub const PRIORITY_INVERSION_HIGH_COUNT: usize = 1;
pub const PRIORITY_INVERSION_MEDIUM_COUNT: usize = 1;
pub const PRIORITY_INVERSION_LOW_COUNT: usize = 1;
pub const PRIORITY_INVERSION_HOLD_ITERS: u64 = 4096;
pub const PRIORITY_INVERSION_WORK_ITERS: u64 = 1024;
pub const PRIORITY_INVERSION_PI_MODE: super::FutexLockMode = super::FutexLockMode::Plain;
pub const PRODUCER_CONSUMER_PRODUCERS: usize = 2;
pub const PRODUCER_CONSUMER_CONSUMERS: usize = 1;
pub const PRODUCER_CONSUMER_PRODUCE_RATE_HZ: u64 = 1_000;
pub const PRODUCER_CONSUMER_CONSUME_ITERS: u64 = 4_096;
pub const PRODUCER_CONSUMER_QUEUE_DEPTH_TARGET: u64 = 1024;
pub const RT_STARVATION_RT_WORKERS: usize = 1;
pub const RT_STARVATION_CFS_WORKERS: usize = 1;
pub const RT_STARVATION_RT_PRIORITY: i32 = 50;
pub const RT_STARVATION_BURST_ITERS: u64 = 1024;
pub const ASYMMETRIC_WAKER_BURST_ITERS: u64 = 1024;
pub const WAKE_CHAIN_DEPTH: usize = 4;
pub const WAKE_CHAIN_WAKE: super::WakeMechanism = super::WakeMechanism::Pipe;
pub const WAKE_CHAIN_WORK_PER_HOP: std::time::Duration = std::time::Duration::from_micros(100);
pub const NUMA_WORKING_SET_SWEEP_REGION_KB: usize = 4_096;
pub const NUMA_WORKING_SET_SWEEP_SWEEP_PERIOD_MS: u64 = 100;
pub const CGROUP_CHURN_GROUPS: usize = 2;
pub const CGROUP_CHURN_CYCLE_MS: u64 = 100;
pub const SIGNAL_STORM_SIGNALS_PER_ITER: u64 = 16;
pub const SIGNAL_STORM_WORK_ITERS: u64 = 1024;
pub const PREEMPT_STORM_CFS_WORKERS: usize = 2;
pub const PREEMPT_STORM_RT_BURST_ITERS: u64 = 1024;
pub const PREEMPT_STORM_RT_SLEEP_US: u64 = 1_000;
pub const EPOLL_STORM_PRODUCERS: usize = 1;
pub const EPOLL_STORM_CONSUMERS: usize = 2;
pub const EPOLL_STORM_EVENTS_PER_BURST: u64 = 32;
pub const NUMA_MIGRATION_CHURN_PERIOD_MS: u64 = 100;
pub const IDLE_CHURN_BURST_DURATION: std::time::Duration = std::time::Duration::from_millis(1);
pub const IDLE_CHURN_SLEEP_DURATION: std::time::Duration = std::time::Duration::from_millis(5);
pub const IDLE_CHURN_PRECISE_TIMING: bool = false;
pub const ALU_HOT_WIDTH: super::AluWidth = super::AluWidth::Widest;
pub const IPC_VARIANCE_HOT_ITERS: u64 = 100_000;
pub const IPC_VARIANCE_COLD_ITERS: u64 = 1024;
pub const IPC_VARIANCE_PERIOD_ITERS: u64 = 64;
}
pub(crate) fn resolve_work_type(
base: &WorkType,
override_wt: Option<&WorkType>,
swappable: bool,
num_workers: usize,
) -> WorkType {
if !swappable {
return base.clone();
}
match override_wt {
Some(wt) => {
if let Some(gs) = wt.worker_group_size()
&& !num_workers.is_multiple_of(gs)
{
return base.clone();
}
wt.clone()
}
None => base.clone(),
}
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SchedPolicy {
Normal,
Batch,
Idle,
Fifo(u32),
RoundRobin(u32),
Deadline {
#[serde(with = "humantime_serde_helper")]
runtime: Duration,
#[serde(with = "humantime_serde_helper")]
deadline: Duration,
#[serde(with = "humantime_serde_helper")]
period: Duration,
},
}
impl SchedPolicy {
pub fn fifo(priority: u32) -> Self {
SchedPolicy::Fifo(priority)
}
pub fn round_robin(priority: u32) -> Self {
SchedPolicy::RoundRobin(priority)
}
pub fn deadline(runtime: Duration, deadline: Duration, period: Duration) -> Self {
SchedPolicy::Deadline {
runtime,
deadline,
period,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FutexLockMode {
Pi,
#[default]
Plain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WakeMechanism {
#[default]
Pipe,
Futex,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AluWidth {
#[default]
Scalar,
Vec128,
Vec256,
Vec512,
Amx,
Widest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SchedClass {
#[default]
Cfs,
Batch,
Idle,
Rt,
Deadline,
Ext,
}
const RT_DEFAULT_PRIO: u32 = 50;
impl SchedClass {
pub fn to_policy(self) -> SchedPolicy {
match self {
SchedClass::Cfs | SchedClass::Ext => SchedPolicy::Normal,
SchedClass::Batch => SchedPolicy::Batch,
SchedClass::Idle => SchedPolicy::Idle,
SchedClass::Rt => SchedPolicy::Fifo(RT_DEFAULT_PRIO),
SchedClass::Deadline => Self::default_deadline_reservation(),
}
}
pub fn default_deadline_reservation() -> SchedPolicy {
SchedPolicy::Deadline {
runtime: Duration::from_micros(1),
deadline: Duration::from_millis(1),
period: Duration::from_millis(10),
}
}
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemPolicy {
#[default]
Default,
Bind(BTreeSet<usize>),
Preferred(usize),
Interleave(BTreeSet<usize>),
Local,
PreferredMany(BTreeSet<usize>),
WeightedInterleave(BTreeSet<usize>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct MpolFlags(u32);
impl MpolFlags {
pub const NONE: Self = Self(0);
pub const STATIC_NODES: Self = Self(1 << 15);
pub const RELATIVE_NODES: Self = Self(1 << 14);
pub const NUMA_BALANCING: Self = Self(1 << 13);
#[cfg(test)]
pub(crate) const fn from_bits_for_test(bits: u32) -> Self {
Self(bits)
}
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub const fn bits(self) -> u32 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
}
impl std::ops::BitOr for MpolFlags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CloneMode {
#[default]
Fork,
Thread,
}
impl MemPolicy {
pub fn bind(nodes: impl IntoIterator<Item = usize>) -> Self {
MemPolicy::Bind(nodes.into_iter().collect())
}
pub fn preferred(node: usize) -> Self {
MemPolicy::Preferred(node)
}
pub fn interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
MemPolicy::Interleave(nodes.into_iter().collect())
}
pub fn preferred_many(nodes: impl IntoIterator<Item = usize>) -> Self {
MemPolicy::PreferredMany(nodes.into_iter().collect())
}
pub fn weighted_interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
MemPolicy::WeightedInterleave(nodes.into_iter().collect())
}
pub fn node_set(&self) -> BTreeSet<usize> {
match self {
MemPolicy::Default | MemPolicy::Local => BTreeSet::new(),
MemPolicy::Bind(nodes)
| MemPolicy::Interleave(nodes)
| MemPolicy::PreferredMany(nodes)
| MemPolicy::WeightedInterleave(nodes) => nodes.clone(),
MemPolicy::Preferred(node) => [*node].into_iter().collect(),
}
}
pub fn validate(&self) -> std::result::Result<(), String> {
match self {
MemPolicy::Default | MemPolicy::Local => Ok(()),
MemPolicy::Preferred(_) => Ok(()),
MemPolicy::Bind(nodes) if nodes.is_empty() => {
Err("Bind policy requires at least one NUMA node".into())
}
MemPolicy::Interleave(nodes) if nodes.is_empty() => {
Err("Interleave policy requires at least one NUMA node".into())
}
MemPolicy::PreferredMany(nodes) if nodes.is_empty() => {
Err("PreferredMany policy requires at least one NUMA node".into())
}
MemPolicy::WeightedInterleave(nodes) if nodes.is_empty() => {
Err("WeightedInterleave policy requires at least one NUMA node".into())
}
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(bound(deserialize = ""))]
pub struct WorkloadConfig {
pub num_workers: usize,
pub affinity: AffinityIntent,
pub work_type: WorkType,
pub sched_policy: SchedPolicy,
pub mem_policy: MemPolicy,
pub mpol_flags: MpolFlags,
pub nice: Option<i32>,
pub clone_mode: CloneMode,
pub comm: Option<Cow<'static, str>>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub numa_node: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub composed: Vec<WorkSpec>,
}
impl Default for WorkloadConfig {
fn default() -> Self {
Self {
num_workers: 1,
affinity: AffinityIntent::Inherit,
work_type: WorkType::SpinWait,
sched_policy: SchedPolicy::Normal,
mem_policy: MemPolicy::Default,
mpol_flags: MpolFlags::NONE,
nice: None,
clone_mode: CloneMode::Fork,
comm: None,
uid: None,
gid: None,
numa_node: None,
composed: Vec::new(),
}
}
}
impl WorkloadConfig {
#[must_use = "builder methods consume self; bind the result"]
pub fn workers(mut self, n: usize) -> Self {
self.num_workers = n;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn affinity(mut self, a: AffinityIntent) -> Self {
self.affinity = a;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn work_type(mut self, wt: WorkType) -> Self {
self.work_type = wt;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn sched_policy(mut self, p: SchedPolicy) -> Self {
self.sched_policy = p;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn mem_policy(mut self, p: MemPolicy) -> Self {
self.mem_policy = p;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn mpol_flags(mut self, f: MpolFlags) -> Self {
self.mpol_flags = f;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn nice(mut self, n: i32) -> Self {
self.nice = Some(n);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn clone_mode(mut self, m: CloneMode) -> Self {
self.clone_mode = m;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn comm(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.comm = Some(name.into());
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn uid(mut self, uid: u32) -> Self {
self.uid = Some(uid);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn gid(mut self, gid: u32) -> Self {
self.gid = Some(gid);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn numa_node(mut self, node: u32) -> Self {
self.numa_node = Some(node);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn composed(mut self, specs: impl IntoIterator<Item = WorkSpec>) -> Self {
self.composed = specs.into_iter().collect();
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn with_composed(mut self, spec: WorkSpec) -> Self {
self.composed.push(spec);
self
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(bound(deserialize = ""))]
pub struct WorkSpec {
pub work_type: WorkType,
pub sched_policy: SchedPolicy,
pub num_workers: Option<usize>,
pub affinity: AffinityIntent,
pub mem_policy: MemPolicy,
pub mpol_flags: MpolFlags,
pub nice: Option<i32>,
pub comm: Option<Cow<'static, str>>,
pub pcomm: Option<Cow<'static, str>>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub numa_node: Option<u32>,
}
impl Default for WorkSpec {
fn default() -> Self {
Self {
work_type: WorkType::SpinWait,
sched_policy: SchedPolicy::Normal,
num_workers: None,
affinity: AffinityIntent::Inherit,
mem_policy: MemPolicy::Default,
mpol_flags: MpolFlags::NONE,
nice: None,
comm: None,
pcomm: None,
uid: None,
gid: None,
numa_node: None,
}
}
}
impl WorkSpec {
#[must_use = "builder methods consume self; bind the result"]
pub fn workers(mut self, n: usize) -> Self {
self.num_workers = Some(n);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn work_type(mut self, wt: WorkType) -> Self {
self.work_type = wt;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn sched_policy(mut self, p: SchedPolicy) -> Self {
self.sched_policy = p;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn affinity(mut self, a: AffinityIntent) -> Self {
self.affinity = a;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn mem_policy(mut self, p: MemPolicy) -> Self {
self.mem_policy = p;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn mpol_flags(mut self, f: MpolFlags) -> Self {
self.mpol_flags = f;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn nice(mut self, n: i32) -> Self {
self.nice = Some(n);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn comm(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.comm = Some(name.into());
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn uid(mut self, uid: u32) -> Self {
self.uid = Some(uid);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn gid(mut self, gid: u32) -> Self {
self.gid = Some(gid);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn numa_node(mut self, node: u32) -> Self {
self.numa_node = Some(node);
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn pcomm(mut self, name: impl Into<Cow<'static, str>>) -> Self {
let name: Cow<'static, str> = name.into();
assert!(
!name.is_empty(),
"WorkSpec::pcomm: empty pcomm string rejected — \
use `None` (default) for no pcomm, not an empty value",
);
assert!(
!name.contains('\0'),
"WorkSpec::pcomm: pcomm string {name:?} contains an interior NUL byte; \
prctl(PR_SET_NAME) treats it as a C string and would truncate \
at the NUL — strip it before calling .pcomm()",
);
self.pcomm = Some(name);
self
}
}
#[cfg(test)]
mod tests {
use super::super::types::WorkType;
use super::*;
use std::collections::BTreeSet;
#[test]
fn sched_policy_debug_shows_variant_and_priority() {
let s = format!("{:?}", SchedPolicy::Fifo(50));
assert!(s.contains("Fifo"), "must show variant name");
assert!(s.contains("50"), "must show priority value");
let s = format!("{:?}", SchedPolicy::RoundRobin(99));
assert!(s.contains("RoundRobin"), "must show variant name");
assert!(s.contains("99"), "must show priority value");
let s1 = format!("{:?}", SchedPolicy::Fifo(1));
let s10 = format!("{:?}", SchedPolicy::Fifo(10));
assert_ne!(
s1, s10,
"different priorities must produce different debug output"
);
}
#[test]
fn sched_policy_copy_preserves_priority() {
let a = SchedPolicy::Fifo(42);
let b = a; match b {
SchedPolicy::Fifo(p) => assert_eq!(p, 42),
_ => panic!("copy must preserve variant and priority"),
}
}
#[test]
fn sched_policy_fifo_constructor() {
match SchedPolicy::fifo(50) {
SchedPolicy::Fifo(p) => assert_eq!(p, 50),
_ => panic!("expected Fifo"),
}
}
#[test]
fn sched_policy_rr_constructor() {
match SchedPolicy::round_robin(25) {
SchedPolicy::RoundRobin(p) => assert_eq!(p, 25),
_ => panic!("expected RoundRobin"),
}
}
#[test]
fn mempolicy_default_node_set_empty() {
assert!(MemPolicy::Default.node_set().is_empty());
}
#[test]
fn mempolicy_local_node_set_empty() {
assert!(MemPolicy::Local.node_set().is_empty());
}
#[test]
fn mempolicy_bind_node_set() {
let p = MemPolicy::Bind([0, 2].into_iter().collect());
assert_eq!(p.node_set(), [0, 2].into_iter().collect());
}
#[test]
fn mempolicy_preferred_node_set() {
let p = MemPolicy::Preferred(1);
assert_eq!(p.node_set(), [1].into_iter().collect());
}
#[test]
fn mempolicy_interleave_node_set() {
let p = MemPolicy::Interleave([0, 1, 3].into_iter().collect());
assert_eq!(p.node_set(), [0, 1, 3].into_iter().collect());
}
#[test]
fn mempolicy_preferred_many_node_set() {
let p = MemPolicy::preferred_many([0, 2]);
assert_eq!(p.node_set(), [0, 2].into_iter().collect());
}
#[test]
fn mempolicy_weighted_interleave_node_set() {
let p = MemPolicy::weighted_interleave([1, 3]);
assert_eq!(p.node_set(), [1, 3].into_iter().collect());
}
#[test]
fn mempolicy_validate_preferred_many_empty() {
assert!(
MemPolicy::PreferredMany(BTreeSet::new())
.validate()
.is_err()
);
}
#[test]
fn mempolicy_validate_weighted_interleave_empty() {
assert!(
MemPolicy::WeightedInterleave(BTreeSet::new())
.validate()
.is_err()
);
}
#[test]
fn mempolicy_validate_preferred_many_ok() {
assert!(MemPolicy::preferred_many([0]).validate().is_ok());
}
#[test]
fn mempolicy_validate_weighted_interleave_ok() {
assert!(MemPolicy::weighted_interleave([0, 1]).validate().is_ok());
}
#[test]
fn mpol_flags_union() {
let f = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
assert_eq!(f.bits(), (1 << 15) | (1 << 13));
}
#[test]
fn mpol_flags_none_is_zero() {
assert_eq!(MpolFlags::NONE.bits(), 0);
}
#[test]
fn work_mpol_flags_builder() {
let w = WorkSpec::default().mpol_flags(MpolFlags::STATIC_NODES);
assert_eq!(w.mpol_flags, MpolFlags::STATIC_NODES);
}
#[test]
fn mpol_flags_contains_identity() {
assert!(MpolFlags::NONE.contains(MpolFlags::NONE));
assert!(MpolFlags::STATIC_NODES.contains(MpolFlags::STATIC_NODES));
let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
assert!(composite.contains(composite));
}
#[test]
fn mpol_flags_contains_superset_is_true_for_subset() {
let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
assert!(composite.contains(MpolFlags::STATIC_NODES));
assert!(composite.contains(MpolFlags::NUMA_BALANCING));
}
#[test]
fn mpol_flags_contains_subset_is_false_for_superset() {
let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
assert!(!MpolFlags::STATIC_NODES.contains(composite));
assert!(!MpolFlags::NUMA_BALANCING.contains(composite));
}
#[test]
fn mpol_flags_contains_empty_is_always_true() {
assert!(MpolFlags::NONE.contains(MpolFlags::NONE));
assert!(MpolFlags::STATIC_NODES.contains(MpolFlags::NONE));
let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
assert!(composite.contains(MpolFlags::NONE));
}
#[test]
fn mpol_flags_none_does_not_contain_any_set_flag() {
assert!(!MpolFlags::NONE.contains(MpolFlags::STATIC_NODES));
assert!(!MpolFlags::NONE.contains(MpolFlags::RELATIVE_NODES));
assert!(!MpolFlags::NONE.contains(MpolFlags::NUMA_BALANCING));
}
#[test]
fn mpol_flags_contains_rejects_disjoint_flag() {
assert!(!MpolFlags::STATIC_NODES.contains(MpolFlags::NUMA_BALANCING));
assert!(!MpolFlags::NUMA_BALANCING.contains(MpolFlags::STATIC_NODES));
}
#[test]
fn mpol_flags_contains_rejects_partial_overlap() {
let a = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
let b = MpolFlags::RELATIVE_NODES | MpolFlags::NUMA_BALANCING;
assert!(!a.contains(b));
assert!(!b.contains(a));
}
#[test]
fn clone_mode_default_is_fork() {
assert!(matches!(CloneMode::default(), CloneMode::Fork));
}
#[test]
fn workload_config_default_clone_mode_is_fork() {
let c = WorkloadConfig::default();
assert!(matches!(c.clone_mode, CloneMode::Fork));
}
#[test]
fn workload_config_clone_mode_builder() {
let cfg = WorkloadConfig::default().clone_mode(CloneMode::Thread);
assert!(matches!(cfg.clone_mode, CloneMode::Thread));
}
#[test]
fn work_mem_policy_builder() {
let w = WorkSpec::default().mem_policy(MemPolicy::Bind([0].into_iter().collect()));
assert!(matches!(w.mem_policy, MemPolicy::Bind(_)));
}
#[test]
fn work_default_mempolicy_is_default() {
let w = WorkSpec::default();
assert!(matches!(w.mem_policy, MemPolicy::Default));
}
#[test]
fn workload_config_default_mempolicy() {
let wl = WorkloadConfig::default();
assert!(matches!(wl.mem_policy, MemPolicy::Default));
}
#[test]
fn workload_config_default_matcher_fields_are_none() {
let wl = WorkloadConfig::default();
assert!(wl.comm.is_none());
assert!(wl.uid.is_none());
assert!(wl.gid.is_none());
assert!(wl.numa_node.is_none());
}
#[test]
fn workload_config_matcher_field_builders() {
let wl = WorkloadConfig::default()
.comm("ktstr-worker")
.uid(1001)
.gid(1002)
.numa_node(0);
assert_eq!(wl.comm.as_deref(), Some("ktstr-worker"));
assert_eq!(wl.uid, Some(1001));
assert_eq!(wl.gid, Some(1002));
assert_eq!(wl.numa_node, Some(0));
}
#[test]
fn workload_config_default_roundtrips() {
let cfg = WorkloadConfig::default();
let json = serde_json::to_string(&cfg).unwrap();
let back: WorkloadConfig = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn resolve_work_type_not_swappable() {
let base = WorkType::SpinWait;
let over = WorkType::YieldHeavy;
let result = resolve_work_type(&base, Some(&over), false, 4);
assert!(matches!(result, WorkType::SpinWait));
}
#[test]
fn resolve_work_type_swappable_applies_override() {
let base = WorkType::SpinWait;
let over = WorkType::YieldHeavy;
let result = resolve_work_type(&base, Some(&over), true, 4);
assert!(matches!(result, WorkType::YieldHeavy));
}
#[test]
fn resolve_work_type_swappable_no_override() {
let base = WorkType::SpinWait;
let result = resolve_work_type(&base, None, true, 4);
assert!(matches!(result, WorkType::SpinWait));
}
#[test]
fn resolve_work_type_group_size_mismatch() {
let base = WorkType::SpinWait;
let over = WorkType::pipe_io(100); let result = resolve_work_type(&base, Some(&over), true, 3); assert!(matches!(result, WorkType::SpinWait));
}
#[test]
fn resolve_work_type_group_size_match() {
let base = WorkType::SpinWait;
let over = WorkType::pipe_io(100); let result = resolve_work_type(&base, Some(&over), true, 4); assert!(matches!(result, WorkType::PipeIo { .. }));
}
#[test]
fn work_builder_chain() {
let w = WorkSpec::default()
.workers(8)
.work_type(WorkType::bursty(
Duration::from_millis(10),
Duration::from_millis(20),
))
.sched_policy(SchedPolicy::Batch)
.affinity(AffinityIntent::SingleCpu)
.nice(7);
assert_eq!(w.num_workers, Some(8));
if let WorkType::Bursty {
burst_duration,
sleep_duration,
} = w.work_type
{
assert_eq!(burst_duration, Duration::from_millis(10));
assert_eq!(sleep_duration, Duration::from_millis(20));
} else {
panic!("expected Bursty variant; got {:?}", w.work_type);
}
assert!(matches!(w.sched_policy, SchedPolicy::Batch));
assert!(matches!(w.affinity, AffinityIntent::SingleCpu));
assert_eq!(w.nice, Some(7));
}
#[test]
fn work_default_values() {
let w = WorkSpec::default();
assert_eq!(w.num_workers, None);
assert!(matches!(w.work_type, WorkType::SpinWait));
assert!(matches!(w.sched_policy, SchedPolicy::Normal));
assert!(matches!(w.affinity, AffinityIntent::Inherit));
assert_eq!(w.nice, None);
}
}