use super::WorkType;
mod mempolicy;
mod sched;
mod work;
mod workload;
pub use mempolicy::{MemPolicy, MpolFlags};
pub use sched::{AluWidth, FutexLockMode, SchedClass, SchedPolicy, WakeMechanism};
pub use work::WorkSpec;
pub use workload::WorkloadConfig;
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_KIB: usize = 32;
pub const CACHE_PRESSURE_STRIDE: usize = 64;
pub const CACHE_YIELD_SIZE_KIB: usize = 32;
pub const CACHE_YIELD_STRIDE: usize = 64;
pub const CACHE_PIPE_SIZE_KIB: 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_KIB: 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_KIB: 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_KIB: 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(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CloneMode {
#[default]
Fork,
Thread,
}
#[cfg(test)]
mod tests {
use super::super::AffinityIntent;
use super::super::types::WorkType;
use super::*;
use std::collections::BTreeSet;
use std::time::Duration;
#[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_bind_empty() {
let err = MemPolicy::Bind(BTreeSet::new()).validate().unwrap_err();
assert!(
err.contains("Bind") && err.contains("NUMA node"),
"diagnostic must name the variant and required content: {err}",
);
assert!(
err.contains("MemPolicy::bind("),
"diagnostic must name the recommended constructor: {err}",
);
}
#[test]
fn mempolicy_validate_interleave_empty() {
let err = MemPolicy::Interleave(BTreeSet::new())
.validate()
.unwrap_err();
assert!(
err.contains("Interleave") && err.contains("NUMA node"),
"diagnostic must name the variant and required content: {err}",
);
assert!(
err.contains("MemPolicy::interleave("),
"diagnostic must name the recommended constructor: {err}",
);
}
#[test]
fn mempolicy_validate_preferred_many_empty() {
let err = MemPolicy::PreferredMany(BTreeSet::new())
.validate()
.unwrap_err();
assert!(
err.contains("PreferredMany") && err.contains("NUMA node"),
"diagnostic must name the variant and required content: {err}",
);
assert!(
err.contains("MemPolicy::preferred_many("),
"diagnostic must name the recommended constructor: {err}",
);
}
#[test]
fn mempolicy_validate_weighted_interleave_empty() {
let err = MemPolicy::WeightedInterleave(BTreeSet::new())
.validate()
.unwrap_err();
assert!(
err.contains("WeightedInterleave") && err.contains("NUMA node"),
"diagnostic must name the variant and required content: {err}",
);
assert!(
err.contains("MemPolicy::weighted_interleave("),
"diagnostic must name the recommended constructor: {err}",
);
assert!(
!err.contains("MemPolicy::Interleave(["),
"diagnostic must not suggest the non-compiling capital-I Interleave variant with a literal array: {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 workload_config_validate_accepts_default() {
WorkloadConfig::default()
.validate()
.expect("WorkloadConfig::default must self-validate (mem_policy=Default)");
}
#[test]
fn workload_config_validate_rejects_invalid_primary_mempolicy() {
let cfg = WorkloadConfig::default().mem_policy(MemPolicy::Bind(BTreeSet::new()));
let err = cfg
.validate()
.expect_err("empty Bind nodemask on primary must reject");
let msg = err.to_string();
assert!(
msg.contains("primary") && msg.contains("Bind") && msg.contains("NUMA node"),
"diagnostic must name the slot (primary), the variant (Bind), and the constraint (NUMA node): got {msg}",
);
}
#[test]
fn workload_config_validate_rejects_invalid_composed_mempolicy() {
let bad = WorkSpec::default()
.work_type(WorkType::SpinWait)
.mem_policy(MemPolicy::Interleave(BTreeSet::new()));
let cfg = WorkloadConfig::default().composed(vec![bad]);
let err = cfg
.validate()
.expect_err("empty Interleave nodemask on composed[0] must reject");
let msg = err.to_string();
assert!(
msg.contains("composed[0]")
&& msg.contains("group_idx 1")
&& msg.contains("Interleave"),
"diagnostic must name composed[0] + group_idx 1 + Interleave: got {msg}",
);
}
#[test]
fn workload_config_validate_accepts_valid_composed_mempolicy() {
let ok = WorkSpec::default()
.work_type(WorkType::SpinWait)
.mem_policy(MemPolicy::Bind([0].into_iter().collect()));
let cfg = WorkloadConfig::default().composed(vec![ok]);
cfg.validate()
.expect("non-empty Bind on composed[0] must validate");
}
#[test]
fn workload_config_validate_short_circuits_first_invalid_composed() {
let valid_spec = WorkSpec::default()
.work_type(WorkType::SpinWait)
.mem_policy(MemPolicy::Bind([0].into_iter().collect()));
let invalid_bind = WorkSpec::default()
.work_type(WorkType::SpinWait)
.mem_policy(MemPolicy::Bind(BTreeSet::new()));
let invalid_interleave = WorkSpec::default()
.work_type(WorkType::SpinWait)
.mem_policy(MemPolicy::Interleave(BTreeSet::new()));
let cfg =
WorkloadConfig::default().composed(vec![valid_spec, invalid_bind, invalid_interleave]);
let err = cfg
.validate()
.expect_err("multi-composed with invalid entries must reject");
let msg = err.to_string();
assert!(
msg.contains("composed[1]"),
"diagnostic must name the FIRST invalid composed entry (composed[1]): got {msg}",
);
assert!(
msg.contains("Bind"),
"diagnostic must name the first failing variant (Bind): got {msg}",
);
assert!(
!msg.contains("composed[2]"),
"short-circuit must not surface the second invalid entry (composed[2]): got {msg}",
);
}
#[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);
}
#[test]
fn sched_policy_constructors_usable_in_const_context() {
const F: SchedPolicy = SchedPolicy::fifo(50);
const RR: SchedPolicy = SchedPolicy::round_robin(99);
const DL: SchedPolicy = SchedPolicy::deadline(
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
);
assert!(matches!(F, SchedPolicy::Fifo(50)));
assert!(matches!(RR, SchedPolicy::RoundRobin(99)));
assert!(matches!(
DL,
SchedPolicy::Deadline {
runtime,
deadline,
period
} if runtime == Duration::from_millis(10)
&& deadline == Duration::from_millis(20)
&& period == Duration::from_millis(30)
));
}
#[test]
fn sched_policy_default_is_normal_and_serde_roundtrip_per_variant() {
let d: SchedPolicy = Default::default();
assert!(matches!(d, SchedPolicy::Normal));
let variants = [
SchedPolicy::Normal,
SchedPolicy::Batch,
SchedPolicy::Idle,
SchedPolicy::Fifo(50),
SchedPolicy::RoundRobin(99),
SchedPolicy::Deadline {
runtime: Duration::from_millis(10),
deadline: Duration::from_millis(20),
period: Duration::from_millis(30),
},
];
for original in &variants {
let bytes = serde_json::to_vec(original).expect("serialize");
let restored: SchedPolicy = serde_json::from_slice(&bytes).expect("deserialize");
assert_eq!(restored, *original, "roundtrip drift for {original:?}");
}
}
}