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}