ktstr/scenario/
cpuset.rs

1//! Cpuset mutation scenario implementations.
2
3use super::Ctx;
4use super::backdrop::Backdrop;
5use super::ops::{CgroupDef, CpusetSpec, HoldSpec, Op, Step, execute_scenario, execute_steps};
6use crate::assert::AssertResult;
7use crate::workload::*;
8use anyhow::Result;
9use std::time::Duration;
10
11fn cgroup_cpuset_apply_midrun_backdrop() -> Backdrop {
12    Backdrop::new()
13        .push_cgroup(CgroupDef::named("cg_0"))
14        .push_cgroup(CgroupDef::named("cg_1"))
15}
16
17fn cgroup_cpuset_apply_midrun_steps(ctx: &Ctx) -> Vec<Step> {
18    vec![
19        Step::new(vec![], ctx.settled_hold(0.5)),
20        Step::new(
21            vec![
22                Op::set_cpuset("cg_0", CpusetSpec::disjoint(0, 2)),
23                Op::set_cpuset("cg_1", CpusetSpec::disjoint(1, 2)),
24            ],
25            HoldSpec::frac(0.5),
26        ),
27    ]
28}
29
30/// Apply disjoint cpusets to two initially unconstrained cgroups mid-run.
31pub fn custom_cgroup_cpuset_apply_midrun(ctx: &Ctx) -> Result<AssertResult> {
32    execute_scenario(
33        ctx,
34        cgroup_cpuset_apply_midrun_backdrop(),
35        cgroup_cpuset_apply_midrun_steps(ctx),
36    )
37}
38
39/// Clear disjoint cpusets from two cgroups mid-run.
40pub fn custom_cgroup_cpuset_clear_midrun(ctx: &Ctx) -> Result<AssertResult> {
41    let backdrop = Backdrop::new()
42        .push_cgroup(CgroupDef::named("cg_0").cpuset(CpusetSpec::disjoint(0, 2)))
43        .push_cgroup(CgroupDef::named("cg_1").cpuset(CpusetSpec::disjoint(1, 2)));
44
45    let steps = vec![
46        Step::new(vec![], ctx.settled_hold(0.5)),
47        Step::new(
48            vec![Op::clear_cpuset("cg_0"), Op::clear_cpuset("cg_1")],
49            HoldSpec::frac(0.5),
50        ),
51    ];
52
53    execute_scenario(ctx, backdrop, steps)
54}
55
56fn cgroup_cpuset_resize_backdrop() -> Backdrop {
57    Backdrop::new()
58        .push_cgroup(CgroupDef::named("cg_0").cpuset(CpusetSpec::range(0.0, 0.5)))
59        .push_cgroup(CgroupDef::named("cg_1").cpuset(CpusetSpec::range(0.5, 1.0)))
60}
61
62fn cgroup_cpuset_resize_steps(ctx: &Ctx) -> Vec<Step> {
63    vec![
64        Step::new(vec![], ctx.settled_hold(1.0 / 3.0)),
65        Step::new(
66            vec![
67                Op::set_cpuset("cg_0", CpusetSpec::range(0.0, 0.25)),
68                Op::set_cpuset("cg_1", CpusetSpec::range(0.25, 1.0)),
69            ],
70            HoldSpec::frac(1.0 / 3.0),
71        ),
72        Step::new(
73            vec![
74                Op::set_cpuset("cg_0", CpusetSpec::range(0.0, 0.75)),
75                Op::set_cpuset("cg_1", CpusetSpec::range(0.75, 1.0)),
76            ],
77            HoldSpec::frac(1.0 / 3.0),
78        ),
79    ]
80}
81
82/// Three-phase cpuset resize: 50/50, 25/75, 75/25.
83pub fn custom_cgroup_cpuset_resize(ctx: &Ctx) -> Result<AssertResult> {
84    if ctx.topo.all_cpus().len() < 4 {
85        return Ok(AssertResult::skip("need >=4 CPUs"));
86    }
87    execute_scenario(
88        ctx,
89        cgroup_cpuset_resize_backdrop(),
90        cgroup_cpuset_resize_steps(ctx),
91    )
92}
93
94/// Swap disjoint cpuset assignments between two cgroups twice.
95pub fn custom_cgroup_cpuset_swap_disjoint(ctx: &Ctx) -> Result<AssertResult> {
96    if ctx.topo.all_cpus().len() < 8 {
97        return Ok(AssertResult::skip("need >=8 CPUs"));
98    }
99
100    let backdrop = Backdrop::new()
101        .push_cgroup(CgroupDef::named("cg_0").cpuset(CpusetSpec::range(0.0, 0.5)))
102        .push_cgroup(CgroupDef::named("cg_1").cpuset(CpusetSpec::range(0.5, 1.0)));
103
104    let steps = vec![
105        Step::new(vec![], ctx.settled_hold(1.0 / 3.0)),
106        Step::new(
107            vec![
108                Op::set_cpuset("cg_0", CpusetSpec::range(0.5, 1.0)),
109                Op::set_cpuset("cg_1", CpusetSpec::range(0.0, 0.5)),
110            ],
111            HoldSpec::frac(1.0 / 3.0),
112        ),
113        Step::new(
114            vec![
115                Op::set_cpuset("cg_0", CpusetSpec::range(0.0, 0.5)),
116                Op::set_cpuset("cg_1", CpusetSpec::range(0.5, 1.0)),
117            ],
118            HoldSpec::frac(1.0 / 3.0),
119        ),
120    ];
121
122    execute_scenario(ctx, backdrop, steps)
123}
124
125/// Disjoint cpusets with oversubscribed SpinWait vs bursty workers.
126pub fn custom_cgroup_cpuset_workload_imbalance(ctx: &Ctx) -> Result<AssertResult> {
127    let mid = ctx.topo.usable_cpus().len() / 2;
128
129    let steps = vec![Step::with_defs(
130        vec![
131            CgroupDef::named("cg_0")
132                .cpuset(CpusetSpec::disjoint(0, 2))
133                .workers(mid * 2),
134            CgroupDef::named("cg_1")
135                .cpuset(CpusetSpec::disjoint(1, 2))
136                .work_type(WorkType::bursty(
137                    Duration::from_millis(50),
138                    Duration::from_millis(100),
139                )),
140        ],
141        ctx.settled_hold(1.0),
142    )];
143
144    execute_steps(ctx, steps)
145}
146
147/// Oversubscribed and bursty cgroups with cpuset narrowing and widening.
148pub fn custom_cgroup_cpuset_change_imbalance(ctx: &Ctx) -> Result<AssertResult> {
149    if ctx.topo.all_cpus().len() < 4 {
150        return Ok(AssertResult::skip("need >=4 CPUs"));
151    }
152
153    let all = ctx.topo.all_cpus();
154    let last = all.len() - 1;
155    let mid = last / 2;
156
157    let narrow = CpusetSpec::exact([all[mid]]);
158
159    let backdrop = Backdrop::new()
160        .push_cgroup(
161            CgroupDef::named("cg_0")
162                .cpuset(CpusetSpec::range(0.0, 0.5))
163                .workers(mid * 2),
164        )
165        .push_cgroup(
166            CgroupDef::named("cg_1")
167                .cpuset(CpusetSpec::range(0.5, 1.0))
168                .workers(2)
169                .work_type(WorkType::bursty(
170                    Duration::from_millis(30),
171                    Duration::from_millis(100),
172                )),
173        );
174
175    let steps = vec![
176        Step::new(vec![], ctx.settled_hold(1.0 / 3.0)),
177        Step::new(
178            vec![Op::set_cpuset("cg_1", narrow)],
179            HoldSpec::frac(1.0 / 3.0),
180        ),
181        Step::new(
182            vec![Op::set_cpuset("cg_1", CpusetSpec::range(0.5, 1.0))],
183            HoldSpec::frac(1.0 / 3.0),
184        ),
185    ];
186
187    execute_scenario(ctx, backdrop, steps)
188}
189
190/// NUMA-scoped cpusets: one cgroup per NUMA node, then swap mid-run.
191///
192/// Requires a 2+ NUMA node topology. Each cgroup is constrained to a
193/// single NUMA node's CPUs, then cpusets are swapped to force cross-NUMA
194/// migration.
195pub fn custom_cgroup_cpuset_numa_swap(ctx: &Ctx) -> Result<AssertResult> {
196    if ctx.topo.num_numa_nodes() < 2 {
197        return Ok(AssertResult::skip("need >=2 NUMA nodes"));
198    }
199
200    let backdrop = Backdrop::new()
201        .push_cgroup(CgroupDef::named("cg_0").cpuset(CpusetSpec::numa(0)))
202        .push_cgroup(CgroupDef::named("cg_1").cpuset(CpusetSpec::numa(1)));
203
204    let steps = vec![
205        Step::new(vec![], ctx.settled_hold(0.5)),
206        Step::new(
207            vec![
208                Op::set_cpuset("cg_0", CpusetSpec::numa(1)),
209                Op::set_cpuset("cg_1", CpusetSpec::numa(0)),
210            ],
211            HoldSpec::frac(0.5),
212        ),
213    ];
214
215    execute_scenario(ctx, backdrop, steps)
216}
217
218/// Disjoint cpusets where a light cgroup gets heavy load added mid-run.
219pub fn custom_cgroup_cpuset_load_shift(ctx: &Ctx) -> Result<AssertResult> {
220    let backdrop = Backdrop::new()
221        .push_cgroup(
222            CgroupDef::named("cg_0")
223                .cpuset(CpusetSpec::disjoint(0, 2))
224                .workers(16),
225        )
226        .push_cgroup(
227            CgroupDef::named("cg_1")
228                .cpuset(CpusetSpec::disjoint(1, 2))
229                .workers(1)
230                .work_type(WorkType::YieldHeavy),
231        );
232
233    let steps = vec![
234        Step::new(vec![], ctx.settled_hold(0.5)),
235        // Phase 2: add heavy step-local load to cg_1. The new workers
236        // die at step teardown — which is what the prior
237        // execute_steps behavior eventually did at scenario end too.
238        Step::new(
239            vec![Op::spawn_workers("cg_1", WorkSpec::default().workers(16))],
240            HoldSpec::frac(0.5),
241        ),
242    ];
243
244    execute_scenario(ctx, backdrop, steps)
245}
246
247#[cfg(test)]
248mod tests {
249    use super::super::ops::Setup;
250    use super::*;
251    use crate::cgroup::CgroupManager;
252    use crate::topology::TestTopology;
253    use std::time::Duration;
254
255    fn ctx_for_test<'a>(cgroups: &'a CgroupManager, topo: &'a TestTopology) -> Ctx<'a> {
256        Ctx {
257            cgroups,
258            topo,
259            duration: Duration::from_secs(6),
260            workers_per_cgroup: 2,
261            sched_pid: Some(1),
262            settle: Duration::from_millis(100),
263            work_type_override: None,
264            assert: crate::assert::Assert::default_checks(),
265            wait_for_map_write: false,
266            current_step: std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)),
267            entry_name: None,
268            variant_hash: 0,
269        }
270    }
271
272    #[test]
273    fn apply_midrun_backdrop_declares_two_cgroups() {
274        let backdrop = cgroup_cpuset_apply_midrun_backdrop();
275        assert_eq!(
276            backdrop.cgroups.len(),
277            2,
278            "Backdrop declares cg_0 and cg_1 as persistent"
279        );
280        assert_eq!(backdrop.cgroups[0].name.as_ref(), "cg_0");
281        assert_eq!(backdrop.cgroups[1].name.as_ref(), "cg_1");
282    }
283
284    #[test]
285    fn apply_midrun_builds_two_phase_steps() {
286        let cgroups = CgroupManager::new("/nonexistent");
287        let topo = TestTopology::from_vm_topology(&crate::vmm::topology::Topology::new(1, 1, 4, 1));
288        let ctx = ctx_for_test(&cgroups, &topo);
289
290        let steps = cgroup_cpuset_apply_midrun_steps(&ctx);
291        assert_eq!(steps.len(), 2, "settle + apply phases");
292
293        assert!(
294            matches!(&steps[0].setup, Setup::Defs(defs) if defs.is_empty()),
295            "phase 1 has no step-local CgroupDefs (cgroups live in the Backdrop)",
296        );
297        assert!(steps[0].ops.is_empty(), "phase 1 is a pure settle — no ops");
298
299        assert!(matches!(steps[0].hold, HoldSpec::Fixed(_)));
300        let phase2_ops = &steps[1].ops;
301        assert_eq!(phase2_ops.len(), 2, "set_cpuset once per cgroup");
302        for op in phase2_ops {
303            assert!(matches!(op, Op::SetCpuset { .. }));
304        }
305        assert!(matches!(steps[1].hold, HoldSpec::Frac(f) if (f - 0.5).abs() < f64::EPSILON));
306    }
307
308    #[test]
309    fn resize_backdrop_declares_two_cgroups_with_cpusets() {
310        let backdrop = cgroup_cpuset_resize_backdrop();
311        assert_eq!(backdrop.cgroups.len(), 2);
312        assert!(backdrop.cgroups[0].cpuset.is_some());
313        assert!(backdrop.cgroups[1].cpuset.is_some());
314    }
315
316    #[test]
317    fn resize_builds_three_phase_range_progression() {
318        let cgroups = CgroupManager::new("/nonexistent");
319        let topo = TestTopology::from_vm_topology(&crate::vmm::topology::Topology::new(1, 1, 4, 1));
320        let ctx = ctx_for_test(&cgroups, &topo);
321
322        let steps = cgroup_cpuset_resize_steps(&ctx);
323        assert_eq!(steps.len(), 3);
324        assert!(
325            matches!(&steps[0].setup, Setup::Defs(defs) if defs.is_empty()),
326            "phase 1 is a settle step — cgroups live in the Backdrop",
327        );
328        assert!(steps[0].ops.is_empty(), "phase 1 has no ops");
329        // Phases 2 and 3: each reassigns cpusets on both cgroups.
330        for step in &steps[1..] {
331            assert_eq!(step.ops.len(), 2);
332            for op in &step.ops {
333                assert!(matches!(op, Op::SetCpuset { .. }));
334            }
335        }
336    }
337}