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;