ktstr/workload/types/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2//! WorkType, WorkPhase, and WorkTypeValidationError — pure data types
3//! and pure-self methods extracted from the parent workload module.
4//!
5//! Re-exported by the parent module so external paths remain
6//! `crate::workload::WorkType` etc. — the split is internal.
7
8use std::time::Duration;
9
10use crate::workload::config::{AluWidth, humantime_serde_helper};
11
12/// A single phase in a [`WorkType::Sequence`] compound work pattern.
13///
14/// Workers loop through all phases in order, then repeat. Each phase
15/// runs for its specified duration before advancing to the next.
16#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum WorkPhase {
19 /// CPU spin for the given duration.
20 Spin(#[serde(with = "humantime_serde_helper")] Duration),
21 /// Sleep (thread::sleep) for the given duration.
22 Sleep(#[serde(with = "humantime_serde_helper")] Duration),
23 /// Yield (sched_yield) repeatedly for the given duration.
24 Yield(#[serde(with = "humantime_serde_helper")] Duration),
25 /// Simulated I/O (write 64 KB to a tempfile + 100 us sleep) for
26 /// the given duration. The tempfile lives on whatever filesystem
27 /// `std::env::temp_dir()` returns; on the ktstr guest's tmpfs the
28 /// write is a page-cache memcpy and the sleep provides the
29 /// blocking behavior that real disk fsync would cause.
30 /// `WorkType::IoSyncWrite` (the standalone variant) is the disk-IO
31 /// counterpart that opens `/dev/vda` directly.
32 Io(#[serde(with = "humantime_serde_helper")] Duration),
33 /// ALU-bound multiply chain for the given duration. The width
34 /// selector picks the data path the same way as
35 /// [`WorkType::AluHot`] — see [`AluWidth`] for the resolution
36 /// rules and the AVX-512 / AMX caveats. Each visit runs
37 /// `alu_hot_chain` in 1024-step batches in a deadline-bounded
38 /// loop so shutdown latency stays bounded by one batch.
39 ///
40 /// The composable counterpart to [`WorkType::AluHot`]: use this
41 /// inside a [`Sequence`](WorkType::Sequence) to express duty-cycle
42 /// patterns ("ALU 90 % / Sleep 10 %") that the standalone
43 /// [`WorkType::AluHot`] cannot, since the standalone variant
44 /// runs ALU work for the entire scenario duration.
45 AluHot {
46 /// SIMD / scalar width selector for the multiply chain;
47 /// resolved per phase visit via `resolve_alu_width`. See
48 /// [`AluWidth`] for the per-variant data-path width and
49 /// the runtime resolution rules.
50 width: AluWidth,
51 /// Wall-clock duration of the ALU phase. Workers retire
52 /// `alu_hot_chain` batches until this deadline passes.
53 #[serde(with = "humantime_serde_helper")]
54 duration: Duration,
55 },
56}
57
58impl WorkPhase {
59 /// Construct a [`WorkPhase::AluHot`] variant. Sugar for the
60 /// struct-literal form that brings the struct variant in line
61 /// with the tuple-variant constructors — every other [`WorkPhase`]
62 /// variant takes a single `Duration` and reads as
63 /// `WorkPhase::Spin(d)` etc.; the struct variant needs an explicit
64 /// `WorkPhase::AluHot { width: ..., duration: ... }` block at every
65 /// call site, breaking the visual symmetry. `WorkPhase::alu_hot(w, d)`
66 /// restores it so `vec![WorkPhase::Spin(d1), WorkPhase::alu_hot(w, d2),
67 /// WorkPhase::Sleep(d3)]` reads consistently.
68 pub const fn alu_hot(width: AluWidth, duration: Duration) -> Self {
69 WorkPhase::AluHot { width, duration }
70 }
71}
72mod methods;
73mod work_type;
74
75pub use work_type::{CustomCfg, CustomFn, WorkType, WorkerCtx};
76
77/// Spawn-time validation failures for [`WorkType`] preconditions.
78///
79/// Returned (boxed inside [`anyhow::Error`]) by
80/// `WorkloadHandle::spawn` when a per-group `WorkSpec` violates a
81/// runtime invariant the variant doc declares as a precondition.
82/// Tests that need to assert on a specific variant downcast via
83/// `err.downcast_ref::<WorkTypeValidationError>()`; the
84/// `Display` impl carries the same human-readable text the previous
85/// `anyhow::bail!` strings did so call sites that match on the
86/// rendered message keep working.
87///
88/// Each variant carries `group_idx`, a unified group index where 0
89/// is the primary group and the i-th `WorkloadConfig::composed`
90/// entry is `group_idx == i + 1`, so multi-group scenarios can
91/// locate the offending entry without re-parsing the message string. Variants
92/// with multiple constraint inputs (depth, divisor, observed count)
93/// expose those values as named fields to the same end.
94#[derive(Clone, Debug, PartialEq, Eq, Hash, thiserror::Error)]
95pub enum WorkTypeValidationError {
96 /// [`WorkType::IdleChurn`] with `burst_duration == Duration::ZERO`.
97 /// Collapses the per-iteration loop to pure nanosleep so the
98 /// worker accrues no runtime — useless as a scheduler test. See
99 /// the variant doc's "Spawn-time validation" section for the
100 /// full rationale.
101 #[error(
102 "IdleChurn burst_duration must be > 0 (group {group_idx}); a zero \
103 burst makes the loop pure sleep and the worker accrues \
104 no runtime (see [`WorkType::IdleChurn`] variant doc)"
105 )]
106 ZeroBurstDuration {
107 /// Unified group index of the offending group: 0 for the
108 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
109 group_idx: usize,
110 },
111 /// [`WorkType::IdleChurn`] with `sleep_duration == Duration::ZERO`.
112 /// Collapses the per-iteration loop to a CPU-bound burst with
113 /// no idle path; the kernel's `nanosleep(0)` is yield-like
114 /// rather than idle-like. The diagnostic steers the caller to
115 /// [`WorkType::SpinWait`] (pure CPU spin) or
116 /// [`WorkType::YieldHeavy`] (the closer overlap).
117 #[error(
118 "IdleChurn sleep_duration must be > 0 (group {group_idx}); a zero \
119 sleep collapses the loop to a CPU-bound burst. \
120 Use WorkType::SpinWait for pure CPU spin, or \
121 WorkType::YieldHeavy for the closer overlap \
122 (nanosleep(0) is yield-like — see the variant \
123 doc rationale in [`WorkType::IdleChurn`])."
124 )]
125 ZeroSleepDuration {
126 /// Unified group index of the offending group: 0 for the
127 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
128 group_idx: usize,
129 },
130 /// [`WorkType::TimerLatency`] with `interval_us == 0`. A zero interval never
131 /// advances the absolute deadline, collapsing the cyclictest loop to a tight
132 /// busy-spin rather than a timer-latency probe.
133 #[error(
134 "TimerLatency interval_us must be > 0 (group {group_idx}); a zero \
135 interval never advances the deadline and collapses the loop to a \
136 busy-spin (see [`WorkType::TimerLatency`] variant doc)"
137 )]
138 ZeroTimerInterval {
139 /// Unified group index of the offending group: 0 for the
140 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
141 group_idx: usize,
142 },
143 /// [`WorkType::NetTraffic`] with `frame_bytes` outside `[60, 1514]`. Below
144 /// 60 (`ETH_ZLEN`) there is no room for the L2 header the loopback echoes;
145 /// above 1514 (standard MTU + header) the frame exceeds the virtio-net MTU.
146 #[error(
147 "NetTraffic frame_bytes must be in [60, 1514] (got {frame_bytes}, group \
148 {group_idx}); below 60 there is no room for the Ethernet header and \
149 above 1514 exceeds the standard MTU (see [`WorkType::NetTraffic`] \
150 variant doc)"
151 )]
152 NetTrafficFrameBytes {
153 /// The offending `frame_bytes` value the caller supplied.
154 frame_bytes: u16,
155 /// Unified group index of the offending group: 0 for the
156 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
157 group_idx: usize,
158 },
159 /// [`WorkType::IrqWake`] with `frame_bytes` outside `[60, 1514]` — same bound
160 /// and reasoning as [`Self::NetTrafficFrameBytes`] (the IrqWake sender reuses
161 /// the NetTraffic frame builder).
162 #[error(
163 "IrqWake frame_bytes must be in [60, 1514] (got {frame_bytes}, group \
164 {group_idx}); below 60 there is no room for the Ethernet header and \
165 above 1514 exceeds the standard MTU (see [`WorkType::IrqWake`] variant \
166 doc)"
167 )]
168 IrqWakeFrameBytes {
169 /// The offending `frame_bytes` value the caller supplied.
170 frame_bytes: u16,
171 /// Unified group index of the offending group: 0 for the
172 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
173 group_idx: usize,
174 },
175 /// [`WorkType::WakeChain`] with `depth < 2`. A 1-stage chain has
176 /// no successor to wake, and the post-fork close-other-fds
177 /// block would close the worker's own write end (deadlock).
178 #[error(
179 "WakeChain depth must be >= 2 (got {depth}, group {group_idx}); a 1-stage \
180 chain has no successor to wake and the post-fork fd close \
181 logic would close the worker's own write end \
182 (see [`WorkType::WakeChain`] variant doc)"
183 )]
184 InsufficientWakeChainDepth {
185 /// The offending `depth` value the caller supplied.
186 depth: usize,
187 /// Unified group index of the offending group: 0 for the
188 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
189 group_idx: usize,
190 },
191 /// `num_workers` is not a positive multiple of the variant's
192 /// [`worker_group_size`](WorkType::worker_group_size). Affects
193 /// every grouped variant (paired, fan-out, herd, contention,
194 /// chain). The diagnostic names the variant via [`WorkType::name`].
195 #[error(
196 "{name} (group {group_idx}) requires num_workers divisible by {group_size}, got {num_workers}"
197 )]
198 NonDivisibleWorkerCount {
199 /// PascalCase variant name from [`WorkType::name`].
200 name: String,
201 /// Unified group index of the offending group: 0 for the
202 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
203 group_idx: usize,
204 /// Required group size (the variant's
205 /// [`worker_group_size`](WorkType::worker_group_size)).
206 group_size: usize,
207 /// The `num_workers` count the caller supplied.
208 num_workers: usize,
209 },
210 /// [`WorkType::IpcVariance`] with one of `hot_iters`,
211 /// `cold_iters`, or `period_iters` equal to `0`. A zero in
212 /// any of the three collapses the alternation: zero
213 /// `hot_iters` produces a pure cold-phase memory loop, zero
214 /// `cold_iters` produces a pure ALU loop (use
215 /// [`WorkType::AluHot`] directly for that), and zero
216 /// `period_iters` produces a worker that never advances
217 /// past the first stop check. Each rejection names the
218 /// offending field so the caller knows which to fix.
219 #[error(
220 "IpcVariance {field} must be > 0 (group {group_idx}); a zero value \
221 collapses the hot/cold alternation and produces a degenerate \
222 workload (see [`WorkType::IpcVariance`] variant doc)"
223 )]
224 ZeroIpcVarianceParam {
225 /// Static name of the offending field —
226 /// `"hot_iters"`, `"cold_iters"`, or `"period_iters"`.
227 field: &'static str,
228 /// Unified group index of the offending group: 0 for the
229 /// primary group, `i + 1` for `WorkloadConfig::composed[i]`.
230 group_idx: usize,
231 },
232}
233
234#[cfg(test)]
235mod tests;