ktstr/workload/types/methods.rs
1//! Methods on [`super::WorkType`] — naming, parsing, default suggestions,
2//! per-variant worker-group sizing, shared-memory requirements, and
3//! related helpers. Split from the enum catalogue in
4//! [`work_type`](super::work_type) so the impl block can grow without
5//! visually drowning the variant definitions.
6
7use std::time::Duration;
8
9use crate::workload::config::{
10 AluWidth, FutexLockMode, ReapMode, SchedClass, WakeMechanism, defaults,
11};
12use crate::workload::schbench::run::SchbenchConfig;
13use crate::workload::taobench::run::TaobenchConfig;
14
15use crate::workload::WorkerReport;
16
17use super::{CustomCfg, WorkPhase, WorkType, WorkTypeValidationError, WorkerCtx};
18
19impl WorkType {
20 /// PascalCase names for all built-in variants, matching the enum arm names.
21 ///
22 /// Generated by `strum::VariantNames` at compile time from the
23 /// `WorkType` enum definition, so a new variant appears here
24 /// automatically. Includes `"Sequence"` and `"Custom"` even though
25 /// [`from_name`](Self::from_name) cannot construct them (sequences
26 /// require explicit phases; custom requires a function pointer).
27 pub const ALL_NAMES: &'static [&'static str] = <Self as strum::VariantNames>::VARIANTS;
28
29 /// PascalCase name of this variant, matching [`ALL_NAMES`](Self::ALL_NAMES).
30 /// For [`Custom`](Self::Custom), returns the user-provided `name`
31 /// field instead.
32 pub fn name(&self) -> &str {
33 match self {
34 WorkType::SpinWait => "SpinWait",
35 WorkType::YieldHeavy => "YieldHeavy",
36 WorkType::Mixed => "Mixed",
37 WorkType::IoSyncWrite => "IoSyncWrite",
38 WorkType::IoRandRead => "IoRandRead",
39 WorkType::IoConvoy => "IoConvoy",
40 WorkType::Bursty { .. } => "Bursty",
41 WorkType::PipeIo { .. } => "PipeIo",
42 WorkType::FutexPingPong { .. } => "FutexPingPong",
43 WorkType::CachePressure { .. } => "CachePressure",
44 WorkType::CacheYield { .. } => "CacheYield",
45 WorkType::CachePipe { .. } => "CachePipe",
46 WorkType::FutexFanOut { .. } => "FutexFanOut",
47 WorkType::Sequence { .. } => "Sequence",
48 WorkType::ForkExit => "ForkExit",
49 WorkType::NiceSweep => "NiceSweep",
50 WorkType::AffinityChurn { .. } => "AffinityChurn",
51 WorkType::CrossAffinityChurn { .. } => "CrossAffinityChurn",
52 WorkType::PolicyChurn { .. } => "PolicyChurn",
53 WorkType::FanOutCompute { .. } => "FanOutCompute",
54 WorkType::Schbench { .. } => "Schbench",
55 WorkType::Taobench { .. } => "Taobench",
56 WorkType::PageFaultChurn { .. } => "PageFaultChurn",
57 WorkType::MutexContention { .. } => "MutexContention",
58 WorkType::Custom { name, .. } => name.as_str(),
59 WorkType::ThunderingHerd { .. } => "ThunderingHerd",
60 WorkType::PriorityInversion { .. } => "PriorityInversion",
61 WorkType::ProducerConsumerImbalance { .. } => "ProducerConsumerImbalance",
62 WorkType::RtStarvation { .. } => "RtStarvation",
63 WorkType::AsymmetricWaker { .. } => "AsymmetricWaker",
64 WorkType::WakeChain { .. } => "WakeChain",
65 WorkType::NumaWorkingSetSweep { .. } => "NumaWorkingSetSweep",
66 WorkType::CgroupChurn { .. } => "CgroupChurn",
67 WorkType::CgroupAttachStorm { .. } => "CgroupAttachStorm",
68 WorkType::SignalStorm { .. } => "SignalStorm",
69 WorkType::PreemptStorm { .. } => "PreemptStorm",
70 WorkType::EpollStorm { .. } => "EpollStorm",
71 WorkType::NumaMigrationChurn { .. } => "NumaMigrationChurn",
72 WorkType::IdleChurn { .. } => "IdleChurn",
73 WorkType::AluHot { .. } => "AluHot",
74 WorkType::SmtSiblingSpin => "SmtSiblingSpin",
75 WorkType::IpcVariance { .. } => "IpcVariance",
76 WorkType::TimerLatency { .. } => "TimerLatency",
77 WorkType::NetTraffic { .. } => "NetTraffic",
78 WorkType::IrqWake { .. } => "IrqWake",
79 }
80 }
81
82 /// Look up a variant by PascalCase name and return it with default
83 /// parameters. Returns `None` for unknown names, `"Sequence"`
84 /// (requires explicit phases), and `"Custom"` (requires a function
85 /// pointer).
86 pub fn from_name(s: &str) -> Option<WorkType> {
87 match s {
88 "SpinWait" => Some(WorkType::SpinWait),
89 "YieldHeavy" => Some(WorkType::YieldHeavy),
90 "Mixed" => Some(WorkType::Mixed),
91 "IoSyncWrite" => Some(WorkType::IoSyncWrite),
92 "IoRandRead" => Some(WorkType::IoRandRead),
93 "IoConvoy" => Some(WorkType::IoConvoy),
94 "Bursty" => Some(WorkType::Bursty {
95 burst_duration: defaults::BURSTY_BURST_DURATION,
96 sleep_duration: defaults::BURSTY_SLEEP_DURATION,
97 }),
98 "PipeIo" => Some(WorkType::PipeIo {
99 burst_iters: defaults::PIPE_IO_BURST_ITERS,
100 }),
101 "FutexPingPong" => Some(WorkType::FutexPingPong {
102 spin_iters: defaults::FUTEX_PING_PONG_SPIN_ITERS,
103 }),
104 "CachePressure" => Some(WorkType::CachePressure {
105 size_kib: defaults::CACHE_PRESSURE_SIZE_KIB,
106 stride: defaults::CACHE_PRESSURE_STRIDE,
107 }),
108 "CacheYield" => Some(WorkType::CacheYield {
109 size_kib: defaults::CACHE_YIELD_SIZE_KIB,
110 stride: defaults::CACHE_YIELD_STRIDE,
111 }),
112 "CachePipe" => Some(WorkType::CachePipe {
113 size_kib: defaults::CACHE_PIPE_SIZE_KIB,
114 burst_iters: defaults::CACHE_PIPE_BURST_ITERS,
115 }),
116 "FutexFanOut" => Some(WorkType::FutexFanOut {
117 fan_out: defaults::FUTEX_FAN_OUT_FAN_OUT,
118 spin_iters: defaults::FUTEX_FAN_OUT_SPIN_ITERS,
119 }),
120 "ForkExit" => Some(WorkType::ForkExit),
121 "NiceSweep" => Some(WorkType::NiceSweep),
122 "AffinityChurn" => Some(WorkType::AffinityChurn {
123 spin_iters: defaults::AFFINITY_CHURN_SPIN_ITERS,
124 }),
125 "CrossAffinityChurn" => Some(WorkType::CrossAffinityChurn {
126 spin_iters: defaults::CROSS_AFFINITY_CHURN_SPIN_ITERS,
127 }),
128 "PolicyChurn" => Some(WorkType::PolicyChurn {
129 spin_iters: defaults::POLICY_CHURN_SPIN_ITERS,
130 }),
131 "FanOutCompute" => Some(WorkType::FanOutCompute {
132 fan_out: defaults::FAN_OUT_COMPUTE_FAN_OUT,
133 cache_footprint_kib: defaults::FAN_OUT_COMPUTE_CACHE_FOOTPRINT_KIB,
134 operations: defaults::FAN_OUT_COMPUTE_OPERATIONS,
135 sleep_usec: defaults::FAN_OUT_COMPUTE_SLEEP_USEC,
136 }),
137 "Schbench" => Some(WorkType::Schbench {
138 config: SchbenchConfig::default(),
139 }),
140 "Taobench" => Some(WorkType::Taobench {
141 config: TaobenchConfig::default(),
142 }),
143 "PageFaultChurn" => Some(WorkType::PageFaultChurn {
144 region_kib: defaults::PAGE_FAULT_CHURN_REGION_KIB,
145 touches_per_cycle: defaults::PAGE_FAULT_CHURN_TOUCHES_PER_CYCLE,
146 spin_iters: defaults::PAGE_FAULT_CHURN_SPIN_ITERS,
147 }),
148 "MutexContention" => Some(WorkType::MutexContention {
149 contenders: defaults::MUTEX_CONTENTION_CONTENDERS,
150 hold_iters: defaults::MUTEX_CONTENTION_HOLD_ITERS,
151 work_iters: defaults::MUTEX_CONTENTION_WORK_ITERS,
152 }),
153 "ThunderingHerd" => Some(WorkType::ThunderingHerd {
154 waiters: defaults::THUNDERING_HERD_WAITERS,
155 batches: defaults::THUNDERING_HERD_BATCHES,
156 inter_batch_ms: defaults::THUNDERING_HERD_INTER_BATCH_MS,
157 }),
158 "PriorityInversion" => Some(WorkType::PriorityInversion {
159 high_count: defaults::PRIORITY_INVERSION_HIGH_COUNT,
160 medium_count: defaults::PRIORITY_INVERSION_MEDIUM_COUNT,
161 low_count: defaults::PRIORITY_INVERSION_LOW_COUNT,
162 hold_iters: defaults::PRIORITY_INVERSION_HOLD_ITERS,
163 work_iters: defaults::PRIORITY_INVERSION_WORK_ITERS,
164 pi_mode: defaults::PRIORITY_INVERSION_PI_MODE,
165 }),
166 "ProducerConsumerImbalance" => Some(WorkType::ProducerConsumerImbalance {
167 producers: defaults::PRODUCER_CONSUMER_PRODUCERS,
168 consumers: defaults::PRODUCER_CONSUMER_CONSUMERS,
169 produce_rate_hz: defaults::PRODUCER_CONSUMER_PRODUCE_RATE_HZ,
170 consume_iters: defaults::PRODUCER_CONSUMER_CONSUME_ITERS,
171 queue_depth_target: defaults::PRODUCER_CONSUMER_QUEUE_DEPTH_TARGET,
172 }),
173 "RtStarvation" => Some(WorkType::RtStarvation {
174 rt_workers: defaults::RT_STARVATION_RT_WORKERS,
175 cfs_workers: defaults::RT_STARVATION_CFS_WORKERS,
176 rt_priority: defaults::RT_STARVATION_RT_PRIORITY,
177 burst_iters: defaults::RT_STARVATION_BURST_ITERS,
178 }),
179 "AsymmetricWaker" => Some(WorkType::AsymmetricWaker {
180 waker_class: SchedClass::default(),
181 wakee_class: SchedClass::default(),
182 burst_iters: defaults::ASYMMETRIC_WAKER_BURST_ITERS,
183 }),
184 "WakeChain" => Some(WorkType::WakeChain {
185 depth: defaults::WAKE_CHAIN_DEPTH,
186 wake: defaults::WAKE_CHAIN_WAKE,
187 work_per_hop: defaults::WAKE_CHAIN_WORK_PER_HOP,
188 }),
189 "NumaWorkingSetSweep" => Some(WorkType::NumaWorkingSetSweep {
190 region_kib: defaults::NUMA_WORKING_SET_SWEEP_REGION_KIB,
191 sweep_period_ms: defaults::NUMA_WORKING_SET_SWEEP_SWEEP_PERIOD_MS,
192 // Empty list — single-node default leaves binding
193 // disabled, matching `node_set()` defaults from
194 // `MemPolicy::Default`. Users opt-in with a node
195 // list via the constructor.
196 target_nodes: Vec::new(),
197 }),
198 "CgroupChurn" => Some(WorkType::CgroupChurn {
199 groups: defaults::CGROUP_CHURN_GROUPS,
200 cycle_ms: defaults::CGROUP_CHURN_CYCLE_MS,
201 }),
202 "CgroupAttachStorm" => Some(WorkType::CgroupAttachStorm {
203 dest: defaults::CGROUP_ATTACH_STORM_DEST.into(),
204 reap: ReapMode::default(),
205 }),
206 "SignalStorm" => Some(WorkType::SignalStorm {
207 signals_per_iter: defaults::SIGNAL_STORM_SIGNALS_PER_ITER,
208 work_iters: defaults::SIGNAL_STORM_WORK_ITERS,
209 }),
210 "PreemptStorm" => Some(WorkType::PreemptStorm {
211 cfs_workers: defaults::PREEMPT_STORM_CFS_WORKERS,
212 rt_burst_iters: defaults::PREEMPT_STORM_RT_BURST_ITERS,
213 rt_sleep_us: defaults::PREEMPT_STORM_RT_SLEEP_US,
214 }),
215 "EpollStorm" => Some(WorkType::EpollStorm {
216 producers: defaults::EPOLL_STORM_PRODUCERS,
217 consumers: defaults::EPOLL_STORM_CONSUMERS,
218 events_per_burst: defaults::EPOLL_STORM_EVENTS_PER_BURST,
219 }),
220 "NumaMigrationChurn" => Some(WorkType::NumaMigrationChurn {
221 period_ms: defaults::NUMA_MIGRATION_CHURN_PERIOD_MS,
222 }),
223 "IdleChurn" => Some(WorkType::IdleChurn {
224 burst_duration: defaults::IDLE_CHURN_BURST_DURATION,
225 sleep_duration: defaults::IDLE_CHURN_SLEEP_DURATION,
226 precise_timing: defaults::IDLE_CHURN_PRECISE_TIMING,
227 }),
228 "AluHot" => Some(WorkType::AluHot {
229 width: defaults::ALU_HOT_WIDTH,
230 }),
231 "SmtSiblingSpin" => Some(WorkType::SmtSiblingSpin),
232 "IpcVariance" => Some(WorkType::IpcVariance {
233 hot_iters: defaults::IPC_VARIANCE_HOT_ITERS,
234 cold_iters: defaults::IPC_VARIANCE_COLD_ITERS,
235 period_iters: defaults::IPC_VARIANCE_PERIOD_ITERS,
236 }),
237 "TimerLatency" => Some(WorkType::TimerLatency {
238 interval_us: defaults::TIMER_LATENCY_INTERVAL_US,
239 }),
240 "NetTraffic" => Some(WorkType::NetTraffic {
241 interval_us: defaults::NET_TRAFFIC_INTERVAL_US,
242 frame_bytes: defaults::NET_TRAFFIC_FRAME_BYTES,
243 }),
244 "IrqWake" => Some(WorkType::IrqWake {
245 interval_us: defaults::IRQ_WAKE_INTERVAL_US,
246 frame_bytes: defaults::IRQ_WAKE_FRAME_BYTES,
247 }),
248 // Sequence requires explicit phases; no default from_name.
249 _ => None,
250 }
251 }
252
253 /// Case-insensitive lookup that returns the canonical PascalCase
254 /// entry from [`ALL_NAMES`](Self::ALL_NAMES) matching the input,
255 /// or `None` when no entry matches.
256 ///
257 /// Distinct from [`from_name`](Self::from_name) in two ways:
258 ///
259 /// 1. It matches case-insensitively, so `"spinwait"` / `"SPINWAIT"`
260 /// / `"SpinWait"` all map to the same canonical `"SpinWait"`.
261 /// 2. It returns the name string rather than a default-parameter
262 /// [`WorkType`] value, so callers can quote the canonical
263 /// spelling in error messages without also instantiating the
264 /// variant.
265 ///
266 /// Intended as a CLI / config-parser helper: when `from_name`
267 /// returns `None` for the user's input, pass the same string
268 /// here to recover the canonical spelling (if any) for a
269 /// friendlier "did you mean `SpinWait`?" diagnostic. Includes
270 /// `"Sequence"` and `"Custom"` in the match space even though
271 /// `from_name` refuses to construct them — the point of
272 /// [`suggest`](Self::suggest) is naming, not construction.
273 ///
274 /// Whitespace handling: the match uses `eq_ignore_ascii_case`
275 /// without trimming, so surrounding whitespace in `s`
276 /// (`" SpinWait"`, `"SpinWait\n"`) suppresses a match. Callers
277 /// that accept user input with possible surrounding whitespace
278 /// must `s.trim()` before calling — the same convention
279 /// `from_name` follows. Keeping the predicate strict here
280 /// avoids confusing "suggested canonical spelling" reports for
281 /// inputs that were already nearly correct save for stray
282 /// whitespace the caller should have already normalized.
283 pub fn suggest(s: &str) -> Option<&'static str> {
284 Self::ALL_NAMES
285 .iter()
286 .copied()
287 .find(|n| n.eq_ignore_ascii_case(s))
288 }
289
290 /// Worker group size for this work type, or None if ungrouped.
291 ///
292 /// `num_workers` must be divisible by this value. Paired types return 2,
293 /// fan-out returns fan_out + 1 (1 messenger + N receivers), and
294 /// MutexContention returns `contenders`.
295 pub fn worker_group_size(&self) -> Option<usize> {
296 match self {
297 WorkType::PipeIo { .. }
298 | WorkType::FutexPingPong { .. }
299 | WorkType::CachePipe { .. } => Some(2),
300 WorkType::FutexFanOut { fan_out, .. } => Some(fan_out + 1),
301 WorkType::FanOutCompute { fan_out, .. } => Some(fan_out + 1),
302 WorkType::MutexContention { contenders, .. } => Some(*contenders),
303 // ThunderingHerd uses a single global futex shared by
304 // every worker — group size must equal num_workers so
305 // the per-group futex allocator yields exactly one
306 // shared region for the whole herd.
307 WorkType::ThunderingHerd { waiters, .. } => Some(waiters + 1),
308 // PriorityInversion: all 3 tiers share the same futex
309 // word, so the group covers every worker.
310 WorkType::PriorityInversion {
311 high_count,
312 medium_count,
313 low_count,
314 ..
315 } => Some(high_count + medium_count + low_count),
316 // ProducerConsumerImbalance: producers + consumers
317 // share the queue mmap.
318 WorkType::ProducerConsumerImbalance {
319 producers,
320 consumers,
321 ..
322 } => Some(producers + consumers),
323 // RtStarvation: rt + cfs workers share the same
324 // affinity-constrained scheduling domain.
325 WorkType::RtStarvation {
326 rt_workers,
327 cfs_workers,
328 ..
329 } => Some(rt_workers + cfs_workers),
330 // AsymmetricWaker: paired waker + wakee (group of 2),
331 // matching FutexPingPong's shape.
332 WorkType::AsymmetricWaker { .. } => Some(2),
333 // WakeChain: each chain has `depth` workers. Group
334 // size is the per-chain size; num_workers must be a
335 // positive multiple of depth, and the spawn-side
336 // derives the parallel-chain count from
337 // `num_workers / depth`.
338 WorkType::WakeChain { depth, .. } => Some(*depth),
339 // SignalStorm: paired (waker / wakee), num_workers
340 // must be even. Each pair shares the partner-tid
341 // exchange region.
342 WorkType::SignalStorm { .. } => Some(2),
343 // PreemptStorm: 1 RT worker + cfs_workers CFS spinners
344 // share the same affinity-constrained scheduling
345 // domain so the RT preempts on the same CPU.
346 WorkType::PreemptStorm { cfs_workers, .. } => Some(cfs_workers + 1),
347 // EpollStorm: producers + consumers share the eventfd
348 // / epoll fd handoff. One group per (producers,
349 // consumers) tuple.
350 WorkType::EpollStorm {
351 producers,
352 consumers,
353 ..
354 } => Some(producers + consumers),
355 // SmtSiblingSpin: paired workers intended to be
356 // pinned to the two SMT siblings of one physical
357 // core. The variant doesn't allocate shared memory;
358 // the group of 2 is what binds the AffinityIntent
359 // resolution to a sibling pair. Pair via
360 // [`AffinityIntent::SmtSiblingPair`] (auto-resolved
361 // from host topology) or [`AffinityIntent::Exact`]
362 // (caller-supplied CPU IDs).
363 WorkType::SmtSiblingSpin => Some(2),
364 // IrqWake: paired sender / receiver (group of 2, even count). pos==0
365 // sends self-addressed frames, pos==1 blocks in recvfrom and records a
366 // liveness sample (block-to-return duration) per delivered frame.
367 WorkType::IrqWake { .. } => Some(2),
368 _ => None,
369 }
370 }
371
372 /// Whether this work type needs a pre-fork shared memory region (MAP_SHARED mmap).
373 ///
374 /// `RtStarvation` opts in even though its body never reads or
375 /// writes the futex word: the spawn-side `(futex_ptr, pos)`
376 /// tuple is the only mechanism that hands the worker its
377 /// per-position index, which `RtStarvation` consumes to
378 /// classify itself as RT or CFS. Allocating a single 4-byte
379 /// MAP_SHARED region per group is the cheapest way to get
380 /// `pos` plumbed through worker_main without a wider dispatch
381 /// contract change. `IrqWake` opts in for the same reason:
382 /// `pos == 0` is the frame sender, `pos == 1` the receiver that
383 /// blocks in `recvfrom` — neither touches the futex word.
384 pub fn needs_shared_mem(&self) -> bool {
385 matches!(
386 self,
387 WorkType::FutexPingPong { .. }
388 | WorkType::FutexFanOut { .. }
389 | WorkType::FanOutCompute { .. }
390 | WorkType::MutexContention { .. }
391 | WorkType::ThunderingHerd { .. }
392 | WorkType::PriorityInversion { .. }
393 | WorkType::ProducerConsumerImbalance { .. }
394 | WorkType::AsymmetricWaker { .. }
395 | WorkType::WakeChain { .. }
396 | WorkType::RtStarvation { .. }
397 | WorkType::SignalStorm { .. }
398 | WorkType::PreemptStorm { .. }
399 | WorkType::EpollStorm { .. }
400 | WorkType::IrqWake { .. }
401 )
402 }
403
404 /// Number of pipes per chain that the spawn-side must allocate
405 /// for this work type, or `None` when no per-stage pipe ring is
406 /// needed. The returned `depth` matches the variant's `depth`
407 /// field for `WakeChain { wake: WakeMechanism::Pipe, .. }`;
408 /// every other variant (and `WakeChain` with
409 /// `wake: WakeMechanism::Futex`) returns `None`.
410 ///
411 /// When this returns `Some(depth)`, the spawn-side allocates
412 /// `depth` pipes per chain so stage `i` holds
413 /// `pipe[i].write_end` (to wake stage `i + 1`) and
414 /// `pipe[(i + depth - 1) % depth].read_end` (predecessor's
415 /// wake). `WakeMechanism::Futex` keeps the existing futex-word
416 /// ring and returns `None`.
417 pub fn chain_pipe_depth(&self) -> Option<usize> {
418 match self {
419 WorkType::WakeChain {
420 wake: WakeMechanism::Pipe,
421 depth,
422 ..
423 } => Some(*depth),
424 _ => None,
425 }
426 }
427
428 /// Whether this work type allocates a per-worker cache buffer post-fork.
429 pub fn needs_cache_buf(&self) -> bool {
430 matches!(
431 self,
432 WorkType::CachePressure { .. }
433 | WorkType::CacheYield { .. }
434 | WorkType::CachePipe { .. }
435 | WorkType::FanOutCompute { .. }
436 )
437 }
438
439 /// Bursty work: CPU burst for `burst_duration`, sleep for
440 /// `sleep_duration`, repeat.
441 ///
442 /// Validation fires at spawn time, not construction time; see
443 /// [`WorkType::Bursty`] variant doc for preconditions.
444 pub const fn bursty(burst_duration: Duration, sleep_duration: Duration) -> Self {
445 WorkType::Bursty {
446 burst_duration,
447 sleep_duration,
448 }
449 }
450
451 /// Paired pipe I/O with CPU burst between exchanges.
452 ///
453 /// Validation fires at spawn time, not construction time; see
454 /// [`WorkType::PipeIo`] variant doc for preconditions.
455 pub const fn pipe_io(burst_iters: u64) -> Self {
456 WorkType::PipeIo { burst_iters }
457 }
458
459 /// Paired futex ping-pong with CPU spin between wakes.
460 ///
461 /// Validation fires at spawn time, not construction time; see
462 /// [`WorkType::FutexPingPong`] variant doc for preconditions.
463 pub const fn futex_ping_pong(spin_iters: u64) -> Self {
464 WorkType::FutexPingPong { spin_iters }
465 }
466
467 /// Strided read-modify-write over a `size_kib` KiB buffer.
468 ///
469 /// Validation fires at spawn time, not construction time; see
470 /// [`WorkType::CachePressure`] variant doc for preconditions.
471 pub const fn cache_pressure(size_kib: usize, stride: usize) -> Self {
472 WorkType::CachePressure { size_kib, stride }
473 }
474
475 /// Cache pressure burst followed by sched_yield().
476 ///
477 /// Validation fires at spawn time, not construction time; see
478 /// [`WorkType::CacheYield`] variant doc for preconditions.
479 pub const fn cache_yield(size_kib: usize, stride: usize) -> Self {
480 WorkType::CacheYield { size_kib, stride }
481 }
482
483 /// Cache pressure burst then pipe exchange with a partner worker.
484 ///
485 /// Validation fires at spawn time, not construction time; see
486 /// [`WorkType::CachePipe`] variant doc for preconditions.
487 pub const fn cache_pipe(size_kib: usize, burst_iters: u64) -> Self {
488 WorkType::CachePipe {
489 size_kib,
490 burst_iters,
491 }
492 }
493
494 /// 1:N fan-out wake pattern with CPU spin between wakes.
495 ///
496 /// Validation fires at spawn time, not construction time; see
497 /// [`WorkType::FutexFanOut`] variant doc for preconditions.
498 pub const fn futex_fan_out(fan_out: usize, spin_iters: u64) -> Self {
499 WorkType::FutexFanOut {
500 fan_out,
501 spin_iters,
502 }
503 }
504
505 /// Rapid self-directed affinity changes with `spin_iters` CPU work between.
506 ///
507 /// Validation fires at spawn time, not construction time; see
508 /// [`WorkType::AffinityChurn`] variant doc for preconditions.
509 pub const fn affinity_churn(spin_iters: u64) -> Self {
510 WorkType::AffinityChurn { spin_iters }
511 }
512
513 /// Rapid cross-task affinity churn: each worker rewrites its
514 /// siblings' affinity. Must run in a dedicated cgroup; see the
515 /// [`WorkType::CrossAffinityChurn`] variant doc for preconditions.
516 pub const fn cross_affinity_churn(spin_iters: u64) -> Self {
517 WorkType::CrossAffinityChurn { spin_iters }
518 }
519
520 /// Cycle scheduling policies with `spin_iters` CPU work between switches.
521 ///
522 /// Validation fires at spawn time, not construction time; see
523 /// [`WorkType::PolicyChurn`] variant doc for preconditions.
524 pub const fn policy_churn(spin_iters: u64) -> Self {
525 WorkType::PolicyChurn { spin_iters }
526 }
527
528 /// Messenger/worker fan-out with compute work using the given parameters.
529 ///
530 /// `fan_out` is passed to `futex_wake(ptr, N)` where `N: i32` is
531 /// the number of waiters to wake. Realistic values are tens of
532 /// workers; sched-test topologies that need more than `i32::MAX`
533 /// (~2.1B) receivers per messenger are not expressible.
534 /// The worker's `futex_wake` call site clamps the cast via
535 /// `clamp_futex_wake_n` (`worker/mod.rs`) so a pathological
536 /// `usize` input wakes at most `i32::MAX` waiters instead of
537 /// wrapping to a negative N (FUTEX_WAKE broadcasts when passed a
538 /// negative N on some kernels, which would wake every waiter on
539 /// the futex rather than just this messenger's receivers).
540 ///
541 /// Validation fires at spawn time, not construction time; see
542 /// [`WorkType::FanOutCompute`] variant doc for preconditions.
543 pub const fn fan_out_compute(
544 fan_out: usize,
545 cache_footprint_kib: usize,
546 operations: usize,
547 sleep_usec: u64,
548 ) -> Self {
549 WorkType::FanOutCompute {
550 fan_out,
551 cache_footprint_kib,
552 operations,
553 sleep_usec,
554 }
555 }
556
557 /// The `schbench_rs` workload (schbench's default mode), configured by
558 /// `config`. Build the config with [`SchbenchConfig::default`] plus its
559 /// chainable setters, e.g.
560 /// `WorkType::schbench(SchbenchConfig::default().message_threads(2))`. Use
561 /// with a single ktstr worker; see the [`WorkType::Schbench`] variant doc.
562 pub const fn schbench(config: SchbenchConfig) -> Self {
563 WorkType::Schbench { config }
564 }
565
566 /// The `taobench_rs` workload (a bounded, evicting key-value cache),
567 /// configured by `config`. Build the config with [`TaobenchConfig::default`]
568 /// plus its chainable setters, e.g.
569 /// `WorkType::taobench(TaobenchConfig::default().target_hit_pct(95))`. Use
570 /// with a single ktstr worker; see the [`WorkType::Taobench`] variant doc.
571 pub const fn taobench(config: TaobenchConfig) -> Self {
572 WorkType::Taobench { config }
573 }
574
575 /// Rapid page fault cycling with `spin_iters` CPU work between cycles.
576 ///
577 /// Validation fires at spawn time, not construction time; see
578 /// [`WorkType::PageFaultChurn`] variant doc for preconditions.
579 pub const fn page_fault_churn(
580 region_kib: usize,
581 touches_per_cycle: usize,
582 spin_iters: u64,
583 ) -> Self {
584 WorkType::PageFaultChurn {
585 region_kib,
586 touches_per_cycle,
587 spin_iters,
588 }
589 }
590
591 /// N-way futex mutex contention with `contenders` workers per group.
592 ///
593 /// Validation fires at spawn time, not construction time; see
594 /// [`WorkType::MutexContention`] variant doc for preconditions.
595 pub const fn mutex_contention(contenders: usize, hold_iters: u64, work_iters: u64) -> Self {
596 WorkType::MutexContention {
597 contenders,
598 hold_iters,
599 work_iters,
600 }
601 }
602
603 /// One waker, N waiters on a single global futex; broadcasts via
604 /// `FUTEX_WAKE` per batch. Pairs with
605 /// [`WorkType::ThunderingHerd`].
606 ///
607 /// Validation fires at spawn time, not construction time; see
608 /// [`WorkType::ThunderingHerd`] variant doc for preconditions.
609 pub const fn thundering_herd(waiters: usize, batches: u64, inter_batch_ms: u64) -> Self {
610 WorkType::ThunderingHerd {
611 waiters,
612 batches,
613 inter_batch_ms,
614 }
615 }
616
617 /// Three priority tiers contending for one shared lock. See
618 /// [`WorkType::PriorityInversion`] for behavior; pass
619 /// [`FutexLockMode::Pi`] to invoke `FUTEX_LOCK_PI` or
620 /// [`FutexLockMode::Plain`] for a non-PI futex.
621 ///
622 /// Validation fires at spawn time, not construction time; see
623 /// [`WorkType::PriorityInversion`] variant doc for preconditions.
624 pub const fn priority_inversion(
625 high_count: usize,
626 medium_count: usize,
627 low_count: usize,
628 hold_iters: u64,
629 work_iters: u64,
630 pi_mode: FutexLockMode,
631 ) -> Self {
632 WorkType::PriorityInversion {
633 high_count,
634 medium_count,
635 low_count,
636 hold_iters,
637 work_iters,
638 pi_mode,
639 }
640 }
641
642 /// Producer/consumer pipeline with deliberately unbalanced
643 /// rates. See [`WorkType::ProducerConsumerImbalance`].
644 ///
645 /// Validation fires at spawn time, not construction time; see
646 /// [`WorkType::ProducerConsumerImbalance`] variant doc for preconditions.
647 pub const fn producer_consumer_imbalance(
648 producers: usize,
649 consumers: usize,
650 produce_rate_hz: u64,
651 consume_iters: u64,
652 queue_depth_target: u64,
653 ) -> Self {
654 WorkType::ProducerConsumerImbalance {
655 producers,
656 consumers,
657 produce_rate_hz,
658 consume_iters,
659 queue_depth_target,
660 }
661 }
662
663 /// `rt_workers` SCHED_FIFO workers vs. `cfs_workers` SCHED_NORMAL
664 /// workers competing on the same CPU set. See
665 /// [`WorkType::RtStarvation`].
666 ///
667 /// Validation fires at spawn time, not construction time; see
668 /// [`WorkType::RtStarvation`] variant doc for preconditions.
669 pub const fn rt_starvation(
670 rt_workers: usize,
671 cfs_workers: usize,
672 rt_priority: i32,
673 burst_iters: u64,
674 ) -> Self {
675 WorkType::RtStarvation {
676 rt_workers,
677 cfs_workers,
678 rt_priority,
679 burst_iters,
680 }
681 }
682
683 /// Paired workers in mismatched scheduling classes. See
684 /// [`WorkType::AsymmetricWaker`].
685 ///
686 /// Validation fires at spawn time, not construction time; see
687 /// [`WorkType::AsymmetricWaker`] variant doc for preconditions.
688 pub const fn asymmetric_waker(
689 waker_class: SchedClass,
690 wakee_class: SchedClass,
691 burst_iters: u64,
692 ) -> Self {
693 WorkType::AsymmetricWaker {
694 waker_class,
695 wakee_class,
696 burst_iters,
697 }
698 }
699
700 /// Pipeline of waker-wakee hops with optional `WF_SYNC`. See
701 /// [`WorkType::WakeChain`].
702 ///
703 /// Validation fires at spawn time, not construction time; see
704 /// [`WorkType::WakeChain`] variant doc for preconditions
705 /// (`depth >= 2`, `num_workers` divisible by `depth`, etc.).
706 pub const fn wake_chain(depth: usize, wake: WakeMechanism, work_per_hop: Duration) -> Self {
707 WorkType::WakeChain {
708 depth,
709 wake,
710 work_per_hop,
711 }
712 }
713
714 /// NUMA working-set sweep with periodic `mbind` rotation. See
715 /// [`WorkType::NumaWorkingSetSweep`]. `target_nodes` accepts
716 /// any `IntoIterator<Item = usize>` for ergonomic call sites
717 /// (`[0, 1, 2]`, `0..node_count`, `BTreeSet`, etc.).
718 ///
719 /// Validation fires at spawn time, not construction time; see
720 /// [`WorkType::NumaWorkingSetSweep`] variant doc for preconditions.
721 pub fn numa_working_set_sweep(
722 region_kib: usize,
723 sweep_period_ms: u64,
724 target_nodes: impl IntoIterator<Item = usize>,
725 ) -> Self {
726 WorkType::NumaWorkingSetSweep {
727 region_kib,
728 sweep_period_ms,
729 target_nodes: target_nodes.into_iter().collect(),
730 }
731 }
732
733 /// Construct a [`WorkType::Sequence`] from a head phase and an
734 /// iterator of follow-on phases.
735 ///
736 /// The `Sequence` variant cannot use [`from_name`](Self::from_name)
737 /// because phases require explicit construction; this constructor
738 /// is the only typed entry point. Accepts any `IntoIterator<Item =
739 /// WorkPhase>` for `rest` so callers can pass arrays, `Vec`, or
740 /// builder-style chains.
741 ///
742 /// Validation fires at spawn time, not construction time; see
743 /// [`WorkType::Sequence`] variant doc for preconditions.
744 pub fn sequence(first: WorkPhase, rest: impl IntoIterator<Item = WorkPhase>) -> Self {
745 WorkType::Sequence {
746 first,
747 rest: rest.into_iter().collect(),
748 }
749 }
750
751 /// Construct a [`WorkType::CgroupChurn`].
752 ///
753 /// Validation fires at spawn time, not construction time; see
754 /// [`WorkType::CgroupChurn`] variant doc for preconditions.
755 pub const fn cgroup_churn(groups: usize, cycle_ms: u64) -> Self {
756 WorkType::CgroupChurn { groups, cycle_ms }
757 }
758
759 /// Construct a [`WorkType::CgroupAttachStorm`].
760 ///
761 /// `dest` is the sibling cgroup name each forked child is migrated
762 /// into; it must already exist at run time (e.g. created via
763 /// [`Op::add_cgroup`](crate::scenario::ops::Op::add_cgroup)). Non-`const`
764 /// because `Into::<String>::into` allocates (mirrors
765 /// [`custom`](Self::custom)). Validation fires at spawn time, not
766 /// construction time; see the [`WorkType::CgroupAttachStorm`] variant
767 /// doc for preconditions.
768 pub fn cgroup_attach_storm(dest: impl Into<String>, reap: ReapMode) -> Self {
769 WorkType::CgroupAttachStorm {
770 dest: dest.into(),
771 reap,
772 }
773 }
774
775 /// Construct a [`WorkType::SignalStorm`].
776 ///
777 /// Validation fires at spawn time, not construction time; see
778 /// [`WorkType::SignalStorm`] variant doc for preconditions.
779 pub const fn signal_storm(signals_per_iter: u64, work_iters: u64) -> Self {
780 WorkType::SignalStorm {
781 signals_per_iter,
782 work_iters,
783 }
784 }
785
786 /// Construct a [`WorkType::PreemptStorm`].
787 ///
788 /// Validation fires at spawn time, not construction time; see
789 /// [`WorkType::PreemptStorm`] variant doc for preconditions.
790 pub const fn preempt_storm(cfs_workers: usize, rt_burst_iters: u64, rt_sleep_us: u64) -> Self {
791 WorkType::PreemptStorm {
792 cfs_workers,
793 rt_burst_iters,
794 rt_sleep_us,
795 }
796 }
797
798 /// Construct a [`WorkType::EpollStorm`].
799 ///
800 /// Validation fires at spawn time, not construction time; see
801 /// [`WorkType::EpollStorm`] variant doc for preconditions.
802 pub const fn epoll_storm(producers: usize, consumers: usize, events_per_burst: u64) -> Self {
803 WorkType::EpollStorm {
804 producers,
805 consumers,
806 events_per_burst,
807 }
808 }
809
810 /// Construct a [`WorkType::NumaMigrationChurn`].
811 ///
812 /// Validation fires at spawn time, not construction time; see
813 /// [`WorkType::NumaMigrationChurn`] variant doc for preconditions.
814 pub const fn numa_migration_churn(period_ms: u64) -> Self {
815 WorkType::NumaMigrationChurn { period_ms }
816 }
817
818 /// Construct a [`WorkType::IdleChurn`] with the default
819 /// `precise_timing = false`.
820 ///
821 /// # Spawn-time precondition
822 ///
823 /// `burst_duration` and `sleep_duration` must both be
824 /// strictly greater than `Duration::ZERO`. The constructor
825 /// itself accepts any value (no early validation); the
826 /// rejection fires at `WorkloadHandle::spawn` time with an
827 /// actionable bail message naming the offending field. See
828 /// [`WorkType::IdleChurn`] variant doc for the rationale and
829 /// the kernel-source citation.
830 ///
831 /// # `precise_timing`
832 ///
833 /// This constructor sets `precise_timing` to
834 /// [`defaults::IDLE_CHURN_PRECISE_TIMING`] (`false`),
835 /// preserving the inherited `current->timer_slack_ns`
836 /// (~50µs default). To opt into 1ns timer slack, build the
837 /// variant directly via the struct-literal form:
838 /// `WorkType::IdleChurn { burst_duration, sleep_duration,
839 /// precise_timing: true }`. See the variant's
840 /// `precise_timing` field doc for the kernel-side
841 /// mechanism.
842 pub const fn idle_churn(burst_duration: Duration, sleep_duration: Duration) -> Self {
843 WorkType::IdleChurn {
844 burst_duration,
845 sleep_duration,
846 precise_timing: defaults::IDLE_CHURN_PRECISE_TIMING,
847 }
848 }
849
850 /// Construct a [`WorkType::AluHot`] at the given execution
851 /// width.
852 ///
853 /// `AluWidth::Widest` resolves to the widest data-path the
854 /// host supports at worker entry. See [`AluWidth`] for the
855 /// per-variant data-path width and the runtime resolution
856 /// rules.
857 ///
858 /// Validation fires at spawn time, not construction time;
859 /// see [`WorkType::AluHot`] variant doc for preconditions.
860 pub const fn alu_hot(width: AluWidth) -> Self {
861 WorkType::AluHot { width }
862 }
863
864 /// Construct a [`WorkType::TimerLatency`] with an inter-cycle interval
865 /// (`interval_us`, the `cyclictest` `-i` analogue). `interval_us == 0` is
866 /// rejected at spawn (`validate_workload_admission`); pass a non-zero
867 /// period. See the [`WorkType::TimerLatency`] variant doc.
868 pub const fn timer_latency(interval_us: u64) -> Self {
869 WorkType::TimerLatency { interval_us }
870 }
871
872 /// Construct a [`WorkType::NetTraffic`] with an inter-frame interval
873 /// (`interval_us`; `0` = continuous burst) and Ethernet `frame_bytes`.
874 /// `frame_bytes` is validated to `[60, 1514]` at spawn, not here; see the
875 /// [`WorkType::NetTraffic`] variant doc.
876 pub const fn net_traffic(interval_us: u64, frame_bytes: u16) -> Self {
877 WorkType::NetTraffic {
878 interval_us,
879 frame_bytes,
880 }
881 }
882
883 /// Construct a [`WorkType::IrqWake`] — a paired sender/receiver where the
884 /// receiver blocks in `recvfrom` and is woken from NET_RX softirq context.
885 /// `interval_us` paces the sender (default 1000 µs gives the receiver a clean
886 /// empty-queue block per frame; `0` maximizes softirq load into `ksoftirqd`
887 /// but degenerates the wake reservoir); `frame_bytes` is validated to
888 /// `[60, 1514]` at spawn. Spawn an even worker count (group size 2). See the
889 /// [`WorkType::IrqWake`] variant doc.
890 pub const fn irq_wake(interval_us: u64, frame_bytes: u16) -> Self {
891 WorkType::IrqWake {
892 interval_us,
893 frame_bytes,
894 }
895 }
896
897 /// Construct a [`WorkType::IpcVariance`] with explicit hot,
898 /// cold, and period iteration counts.
899 ///
900 /// Returns [`WorkTypeValidationError::ZeroIpcVarianceParam`]
901 /// when any of `hot_iters`, `cold_iters`, or `period_iters`
902 /// is `0`. Construction-time validation matches the
903 /// spawn-time check so callers get immediate feedback at
904 /// the call site rather than discovering the rejection
905 /// only at `WorkloadHandle::spawn` time.
906 pub const fn ipc_variance(
907 hot_iters: u64,
908 cold_iters: u64,
909 period_iters: u64,
910 ) -> std::result::Result<Self, WorkTypeValidationError> {
911 if hot_iters == 0 {
912 return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
913 field: "hot_iters",
914 group_idx: 0,
915 });
916 }
917 if cold_iters == 0 {
918 return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
919 field: "cold_iters",
920 group_idx: 0,
921 });
922 }
923 if period_iters == 0 {
924 return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
925 field: "period_iters",
926 group_idx: 0,
927 });
928 }
929 Ok(WorkType::IpcVariance {
930 hot_iters,
931 cold_iters,
932 period_iters,
933 })
934 }
935
936 /// User-supplied work function with a display name.
937 ///
938 /// `run` receives a [`WorkerCtx`] exposing the worker's stop flag
939 /// (flipped per-mode: the SIGUSR1 handler for `CloneMode::Fork`, a
940 /// per-worker `AtomicBool` for `CloneMode::Thread`) plus its
941 /// effective cpuset, cgroup-sibling pids, own cgroup-v2 dir, and a
942 /// default-zero [`CustomCfg`] payload (use [`Self::custom_with`] to pass
943 /// a non-default cfg), and must return a [`WorkerReport`] when the stop
944 /// flag becomes `true`. The
945 /// framework handles fork / thread spawn, cgroup placement,
946 /// affinity, scheduling policy, and signal setup (Fork mode only);
947 /// `run` owns only the work loop.
948 ///
949 /// The per-iteration built-in instrumentation (wake-latency samples,
950 /// `iter_slot` publish, gap tracking) runs only for built-in variants
951 /// and is bypassed for `Custom`. See the [`Custom`](Self::Custom)
952 /// variant doc for the full telemetry contract and what `run` must
953 /// populate on [`WorkerReport`] to keep downstream assertions honest.
954 pub fn custom(name: impl Into<String>, run: fn(&WorkerCtx) -> WorkerReport) -> Self {
955 WorkType::Custom {
956 name: name.into(),
957 run: super::work_type::CustomFn(run),
958 cfg: CustomCfg::default(),
959 }
960 }
961
962 /// Like [`Self::custom`] but carries a [`CustomCfg`] payload, surfaced
963 /// to the closure via [`WorkerCtx::cfg`]. The payload is Copy POD,
964 /// inherited byte-faithfully across `fork`, so a Custom worker reads its
965 /// per-worker config from `ctx.cfg()` instead of a `static` / global —
966 /// see `tests/preempt_regression.rs` for the shared-futex pattern.
967 pub fn custom_with(
968 name: impl Into<String>,
969 run: fn(&WorkerCtx) -> WorkerReport,
970 cfg: CustomCfg,
971 ) -> Self {
972 WorkType::Custom {
973 name: name.into(),
974 run: super::work_type::CustomFn(run),
975 cfg,
976 }
977 }
978}