ktstr/scenario/
basic.rs

1//! Basic and mixed-workload scenario implementations.
2
3use super::Ctx;
4use super::ops::{CgroupDef, Op, Step, execute_steps};
5use crate::assert::AssertResult;
6use crate::workload::*;
7use anyhow::Result;
8use std::time::Duration;
9
10fn host_cgroup_contention_steps(ctx: &Ctx) -> Vec<Step> {
11    vec![
12        Step::with_defs(
13            vec![CgroupDef::named("cg_0"), CgroupDef::named("cg_1")],
14            ctx.settled_hold(1.0),
15        )
16        .set_ops(vec![Op::spawn_host(
17            WorkSpec::default().workers(ctx.topo.total_cpus()),
18        )]),
19    ]
20}
21
22/// Two managed cgroups with host-level contention from workers in the
23/// test runner's own cgroup (`SpawnPlacement::RunnerCgroup`, typically
24/// the root cgroup). Spawns `total_cpus` workers outside any managed cgroup
25/// alongside two default cgroups.
26pub fn custom_host_cgroup_contention(ctx: &Ctx) -> Result<AssertResult> {
27    execute_steps(ctx, host_cgroup_contention_steps(ctx))
28}
29
30fn sched_mixed_steps(ctx: &Ctx) -> Vec<Step> {
31    let configs = [
32        (SchedPolicy::Normal, WorkType::SpinWait),
33        (SchedPolicy::Batch, WorkType::SpinWait),
34        (SchedPolicy::Idle, WorkType::SpinWait),
35        (
36            SchedPolicy::Fifo(1),
37            WorkType::bursty(Duration::from_millis(500), Duration::from_millis(250)),
38        ),
39    ];
40
41    let mut ops = vec![Op::add_cgroup("cg_0"), Op::add_cgroup("cg_1")];
42    for name in ["cg_0", "cg_1"] {
43        for &(policy, ref wtype) in &configs {
44            ops.push(Op::spawn_workers(
45                name,
46                WorkSpec::default()
47                    .workers(2)
48                    .sched_policy(policy)
49                    .work_type(wtype.clone()),
50            ));
51        }
52    }
53
54    vec![Step::new(ops, ctx.settled_hold(1.0))]
55}
56
57/// Two cgroups each running Normal, Batch, Idle, and FIFO(1) workers
58/// concurrently. FIFO workers use bursty workloads to avoid monopolizing
59/// CPUs.
60pub fn custom_sched_mixed(ctx: &Ctx) -> Result<AssertResult> {
61    execute_steps(ctx, sched_mixed_steps(ctx))
62}
63
64/// Light workload for scheduler-DEATH tests: one cgroup of 4 `YieldHeavy`
65/// workers held for the scenario duration.
66///
67/// Deliberately light — few continuously-runnable tasks — so that when the
68/// sched_ext scheduler dies mid-run, the kernel's scheduler-disable
69/// (`scx_ops_disable`) bypass-drain has trivially few runnable tasks to
70/// migrate. Both death paths funnel through the same drain: an `scx_bpf_error`
71/// crash (host-injected via `bpf_map_write`, fires on any `ktstr_dispatch`
72/// call) AND a watchdog stall (`--stall-after`, which the kernel's
73/// per-rq sched_ext watchdog fires: `scx_watchdog_workfn`'s delayed
74/// workqueue runs `check_rq_for_timeouts` (kernel/sched/ext.c), which
75/// calls `scx_exit(..., SCX_EXIT_ERROR_STALL, ...)` once a runnable
76/// task exceeds `watchdog_timeout`). The scheduler only stops
77/// dispatching under `--stall-after` (`if (stall) return;`); it runs
78/// no timer of its own. A heavy runnable
79/// workload (e.g. [`custom_sched_mixed`]'s 12 SpinWait tasks) triggers the
80/// Linux 6.14 per-node global-DSQ bypass-drain livelock, which strands the
81/// whole guest until the host watchdog fires; that livelock is fixed upstream
82/// only in sched_ext/for-7.1 (per-CPU bypass DSQs + an interruptible aborting
83/// consume loop), so ktstr cannot patch it and instead keeps the death-test
84/// workload light. The death still fires, and the death -> scx exit ->
85/// auto-repro -> wprof-capture path is exercised identically, so no coverage
86/// is lost.
87pub fn custom_crash_light(ctx: &Ctx) -> Result<AssertResult> {
88    let ops = vec![
89        Op::add_cgroup("cg_0"),
90        Op::spawn_workers(
91            "cg_0",
92            WorkSpec::default()
93                .workers(4)
94                .work_type(WorkType::YieldHeavy),
95        ),
96    ];
97    execute_steps(ctx, vec![Step::new(ops, ctx.settled_hold(1.0))])
98}
99
100fn cgroup_pipe_io_steps(ctx: &Ctx) -> Vec<Step> {
101    let mut ops = vec![Op::add_cgroup("cg_0"), Op::add_cgroup("cg_1")];
102    for name in ["cg_0", "cg_1"] {
103        ops.push(Op::spawn_workers(
104            name,
105            WorkSpec::default()
106                .workers(2)
107                .work_type(WorkType::pipe_io(1024)),
108        ));
109        ops.push(Op::spawn_workers(
110            name,
111            WorkSpec::default().workers(ctx.workers_per_cgroup),
112        ));
113    }
114
115    vec![Step::new(ops, ctx.settled_hold(1.0))]
116}
117
118/// Two cgroups each with paired PipeIo workers and SpinWait workers.
119/// Exercises cross-CPU wake placement from pipe I/O under CPU load.
120pub fn custom_cgroup_pipe_io(ctx: &Ctx) -> Result<AssertResult> {
121    execute_steps(ctx, cgroup_pipe_io_steps(ctx))
122}
123
124#[cfg(test)]
125mod tests {
126    use super::super::ops::{Setup, SpawnPlacement};
127    use super::*;
128    use crate::cgroup::CgroupManager;
129    use crate::topology::TestTopology;
130    use std::time::Duration;
131
132    fn ctx_for_test<'a>(cgroups: &'a CgroupManager, topo: &'a TestTopology) -> Ctx<'a> {
133        Ctx {
134            cgroups,
135            topo,
136            duration: Duration::from_secs(1),
137            workers_per_cgroup: 3,
138            sched_pid: Some(1),
139            settle: Duration::from_millis(100),
140            work_type_override: None,
141            assert: crate::assert::Assert::default_checks(),
142            wait_for_map_write: false,
143            current_step: std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)),
144            entry_name: None,
145            variant_hash: 0,
146        }
147    }
148
149    fn def_names(step: &Step) -> Vec<String> {
150        match &step.setup {
151            Setup::Defs(defs) => defs.iter().map(|d| d.name.to_string()).collect(),
152            Setup::Factory(_) => Vec::new(),
153        }
154    }
155
156    #[test]
157    fn host_cgroup_contention_builds_two_defs_and_host_spawn() {
158        let cgroups = CgroupManager::new("/nonexistent");
159        let topo = TestTopology::from_vm_topology(&crate::vmm::topology::Topology::new(1, 1, 4, 1));
160        let ctx = ctx_for_test(&cgroups, &topo);
161
162        let steps = host_cgroup_contention_steps(&ctx);
163        assert_eq!(steps.len(), 1);
164        assert_eq!(def_names(&steps[0]), ["cg_0", "cg_1"]);
165        assert_eq!(steps[0].ops.len(), 1);
166        match &steps[0].ops[0] {
167            Op::Spawn {
168                placement: SpawnPlacement::RunnerCgroup,
169                work,
170            } => {
171                assert_eq!(work.num_workers, Some(topo.total_cpus()));
172            }
173            other => panic!("expected Op::Spawn(RunnerCgroup), got {other:?}"),
174        }
175    }
176
177    #[test]
178    fn sched_mixed_builds_two_add_cgroups_and_eight_spawns() {
179        let cgroups = CgroupManager::new("/nonexistent");
180        let topo = TestTopology::from_vm_topology(&crate::vmm::topology::Topology::new(1, 1, 4, 1));
181        let ctx = ctx_for_test(&cgroups, &topo);
182
183        let steps = sched_mixed_steps(&ctx);
184        assert_eq!(steps.len(), 1);
185        let ops = &steps[0].ops;
186        let adds = ops
187            .iter()
188            .filter(|o| matches!(o, Op::AddCgroup { .. }))
189            .count();
190        let spawns = ops
191            .iter()
192            .filter(|o| {
193                matches!(
194                    o,
195                    Op::Spawn {
196                        placement: SpawnPlacement::Cgroup(_),
197                        ..
198                    }
199                )
200            })
201            .count();
202        assert_eq!(adds, 2, "two cgroups added");
203        assert_eq!(spawns, 8, "4 policies × 2 cgroups = 8 spawns");
204        for op in ops {
205            if let Op::Spawn {
206                placement: SpawnPlacement::Cgroup(_),
207                work,
208            } = op
209            {
210                assert_eq!(work.num_workers, Some(2));
211            }
212        }
213    }
214
215    #[test]
216    fn cgroup_pipe_io_spawn_counts_follow_workers_per_cgroup() {
217        let cgroups = CgroupManager::new("/nonexistent");
218        let topo = TestTopology::from_vm_topology(&crate::vmm::topology::Topology::new(1, 1, 4, 1));
219        let ctx = ctx_for_test(&cgroups, &topo);
220
221        let steps = cgroup_pipe_io_steps(&ctx);
222        assert_eq!(steps.len(), 1);
223        let ops = &steps[0].ops;
224        let spawns: Vec<_> = ops
225            .iter()
226            .filter_map(|o| match o {
227                Op::Spawn {
228                    placement: SpawnPlacement::Cgroup(cgroup),
229                    work,
230                } => Some((cgroup.to_string(), work.num_workers)),
231                _ => None,
232            })
233            .collect();
234        assert_eq!(spawns.len(), 4, "pipe_io spawn + spinwait spawn per cgroup");
235        let spinwait_workers: Vec<_> = spawns
236            .iter()
237            .filter(|(_, n)| *n == Some(ctx.workers_per_cgroup))
238            .collect();
239        assert_eq!(spinwait_workers.len(), 2);
240        let pipe_workers: Vec<_> = spawns.iter().filter(|(_, n)| *n == Some(2)).collect();
241        assert_eq!(pipe_workers.len(), 2);
242    }
243}