ktstr/scenario/
interaction.rs

1//! Cross-cgroup interaction 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
11/// Add a heavy 16-worker cgroup mid-run alongside two light YieldHeavy
12/// cgroups.
13///
14/// `cg_0` and `cg_1` are the steady YieldHeavy residents — they live
15/// for the whole scenario on the Backdrop. `cg_2` joins mid-run as a
16/// step-local CgroupDef and tears down at the step boundary.
17pub fn custom_cgroup_add_load_imbalance(ctx: &Ctx) -> Result<AssertResult> {
18    let backdrop = Backdrop::new()
19        .push_cgroup(
20            CgroupDef::named("cg_0")
21                .workers(1)
22                .work_type(WorkType::YieldHeavy),
23        )
24        .push_cgroup(
25            CgroupDef::named("cg_1")
26                .workers(1)
27                .work_type(WorkType::YieldHeavy),
28        );
29    let steps = vec![
30        // Phase 1: settle with the two steady YieldHeavy cgroups.
31        Step::new(vec![], ctx.settled_hold(0.5)),
32        // Phase 2: add the heavy cg_2 mid-run.
33        Step::with_defs(
34            vec![CgroupDef::named("cg_2").workers(16)],
35            HoldSpec::frac(0.5),
36        ),
37    ];
38
39    execute_scenario(ctx, backdrop, steps)
40}
41
42/// Three cgroups with SpinWait, Bursty, and IoSyncWrite workloads.
43pub fn custom_cgroup_imbalance_mixed_workload(ctx: &Ctx) -> Result<AssertResult> {
44    let steps = vec![Step::with_defs(
45        vec![
46            CgroupDef::named("cg_0").workers(8),
47            ctx.cgroup_def("cg_1").work_type(WorkType::bursty(
48                Duration::from_millis(100),
49                Duration::from_millis(50),
50            )),
51            ctx.cgroup_def("cg_2").work_type(WorkType::IoSyncWrite),
52        ],
53        ctx.settled_hold(1.0),
54    )];
55
56    execute_steps(ctx, steps)
57}
58
59/// Oscillate load between two cgroups across four phases.
60///
61/// Each phase declares fresh step-local `cg_0` / `cg_1` CgroupDefs
62/// with the heavy/light assignment swapped from the prior phase.
63/// The per-Step auto-teardown at every step boundary IS the
64/// "stop + respawn" event — there's no cross-step reference so no
65/// Backdrop is needed. A Backdrop migration would fight the
66/// `Op::MoveAllTasks` Backdrop->step-local guard (dispatch.rs) that
67/// protects persistent workers from step-local teardown, so this
68/// scenario stays on `execute_steps`.
69pub fn custom_cgroup_load_oscillation(ctx: &Ctx) -> Result<AssertResult> {
70    let heavy_cg = |name: &'static str| CgroupDef::named(name).workers(ctx.workers_per_cgroup * 2);
71    let light_cg = |name: &'static str| {
72        CgroupDef::named(name)
73            .workers(1)
74            .work_type(WorkType::YieldHeavy)
75    };
76
77    let mut steps = vec![Step::with_defs(
78        vec![heavy_cg("cg_0"), light_cg("cg_1")],
79        ctx.settled_hold(0.25),
80    )];
81
82    for i in 1..4 {
83        let defs = if i % 2 == 0 {
84            vec![heavy_cg("cg_0"), light_cg("cg_1")]
85        } else {
86            vec![light_cg("cg_0"), heavy_cg("cg_1")]
87        };
88        steps.push(Step::with_defs(defs, HoldSpec::frac(0.25)));
89    }
90
91    execute_steps(ctx, steps)
92}
93
94/// Four cgroups with 16/1/8/4 workers testing multi-cell rebalancing.
95pub fn custom_cgroup_4way_load_imbalance(ctx: &Ctx) -> Result<AssertResult> {
96    if ctx.topo.all_cpus().len() < 5 {
97        return Ok(AssertResult::skip("need >=5 CPUs for 4 cgroups"));
98    }
99
100    let steps = vec![Step::with_defs(
101        vec![
102            CgroupDef::named("cg_0").workers(16),
103            CgroupDef::named("cg_1")
104                .workers(1)
105                .work_type(WorkType::YieldHeavy),
106            CgroupDef::named("cg_2").workers(8),
107            CgroupDef::named("cg_3").workers(4),
108        ],
109        ctx.settled_hold(1.0),
110    )];
111
112    execute_steps(ctx, steps)
113}
114
115/// Disjoint cpusets with oversubscribed SpinWait vs light Bursty workers.
116pub fn custom_cgroup_cpuset_imbalance_combined(ctx: &Ctx) -> Result<AssertResult> {
117    let mid = ctx.topo.usable_cpus().len() / 2;
118
119    let steps = vec![Step::with_defs(
120        vec![
121            CgroupDef::named("cg_0")
122                .cpuset(CpusetSpec::disjoint(0, 2))
123                .workers(mid * 2),
124            CgroupDef::named("cg_1")
125                .cpuset(CpusetSpec::disjoint(1, 2))
126                .workers(2)
127                .work_type(WorkType::bursty(
128                    Duration::from_millis(50),
129                    Duration::from_millis(150),
130                )),
131        ],
132        ctx.settled_hold(1.0),
133    )];
134
135    execute_steps(ctx, steps)
136}
137
138/// Three overlapping cpusets with heavy, bursty, and yield-heavy workers.
139pub fn custom_cgroup_cpuset_overlap_imbalance_combined(ctx: &Ctx) -> Result<AssertResult> {
140    let sets = ctx.topo.overlapping_cpusets(3, 0.5);
141    if sets.iter().any(|s| s.is_empty()) {
142        return Ok(AssertResult::skip("not enough CPUs"));
143    }
144
145    let steps = vec![Step::with_defs(
146        vec![
147            CgroupDef::named("cg_0")
148                .cpuset(CpusetSpec::Exact(sets[0].clone()))
149                .workers(12),
150            CgroupDef::named("cg_1")
151                .cpuset(CpusetSpec::Exact(sets[1].clone()))
152                .workers(2)
153                .work_type(WorkType::bursty(
154                    Duration::from_millis(50),
155                    Duration::from_millis(100),
156                )),
157            CgroupDef::named("cg_2")
158                .cpuset(CpusetSpec::Exact(sets[2].clone()))
159                .workers(1)
160                .work_type(WorkType::YieldHeavy),
161        ],
162        ctx.settled_hold(1.0),
163    )];
164
165    execute_steps(ctx, steps)
166}
167
168/// Workers ping-pong between cg_mobile and cg_1 across 9 MoveAllTasks
169/// phases.
170///
171/// All three cgroups persist for the scenario (the `MoveAllTasks`
172/// ops reference them by name across every Step), so they live in
173/// the [`Backdrop`]. `cg_1` is an empty move target declared via
174/// [`Backdrop::push_op`] so it never spawns its own workers — only
175/// the `cg_mobile` handle ping-pongs between `cg_mobile` and
176/// `cg_1`. Workers that [`Op::MoveAllTasks`] transfers into a
177/// Backdrop cgroup retain their Backdrop ownership so the
178/// persistent workers survive every step teardown.
179pub fn custom_cgroup_no_ctrl_task_migration(ctx: &Ctx) -> Result<AssertResult> {
180    // cg_0: permanent residents. cg_mobile: workers that ping-pong to cg_1.
181    // cg_1: empty move target (no workers). Exactly one handle participates
182    // in MoveAllTasks.
183    let backdrop = Backdrop::new()
184        .push_cgroup(ctx.cgroup_def("cg_0"))
185        .push_cgroup(ctx.cgroup_def("cg_mobile"))
186        .push_op(Op::add_cgroup("cg_1"));
187
188    // Settle: let the Backdrop-spawned workers stabilize before the
189    // first move.
190    let mut steps = vec![Step::new(vec![], HoldSpec::fixed(Duration::from_secs(2)))];
191
192    // 9 ping-pong phases.
193    let mut move_steps: Vec<Step> = (0..9)
194        .map(|i| {
195            let (from, to) = if i % 2 == 0 {
196                ("cg_mobile", "cg_1")
197            } else {
198                ("cg_1", "cg_mobile")
199            };
200            Step::new(vec![Op::move_all_tasks(from, to)], HoldSpec::frac(0.1))
201        })
202        .collect();
203    steps.append(&mut move_steps);
204    // Final hold so workers have residency after the last move.
205    steps.push(Step::new(vec![], HoldSpec::frac(0.1)));
206
207    execute_scenario(ctx, backdrop, steps)
208}
209
210/// Heavy, light, and mobile workers with tasks ping-ponging to overflow
211/// cgroup.
212///
213/// All four cgroups persist — the `MoveAllTasks` ops in every Step
214/// reference `cg_mobile` / `cg_overflow` by name and the permanent
215/// `cg_heavy` / `cg_light` workers run across the whole scenario.
216/// Declaring them in the [`Backdrop`] makes that persistence
217/// explicit and keeps per-step teardown from removing any of them
218/// at a step boundary.
219pub fn custom_cgroup_no_ctrl_imbalance(ctx: &Ctx) -> Result<AssertResult> {
220    // cg_heavy: 6 permanent CPU-spin workers.
221    // cg_light: 2 permanent bursty workers.
222    // cg_mobile: 2 workers that ping-pong to cg_overflow.
223    // cg_overflow: empty move target declared via push_op so it never
224    // spawns workers of its own — only the cg_mobile handle participates
225    // in MoveAllTasks.
226    let backdrop = Backdrop::new()
227        .push_cgroup(CgroupDef::named("cg_heavy").workers(6))
228        .push_cgroup(CgroupDef::named("cg_mobile").workers(2))
229        .push_cgroup(
230            CgroupDef::named("cg_light")
231                .workers(2)
232                .work_type(WorkType::bursty(
233                    Duration::from_millis(50),
234                    Duration::from_millis(100),
235                )),
236        )
237        .push_op(Op::add_cgroup("cg_overflow"));
238
239    let mut steps = vec![Step::new(vec![], HoldSpec::fixed(ctx.settle))];
240
241    let mut move_steps: Vec<Step> = (0..5)
242        .map(|i| {
243            let (from, to) = if i % 2 == 0 {
244                ("cg_mobile", "cg_overflow")
245            } else {
246                ("cg_overflow", "cg_mobile")
247            };
248            Step::new(
249                vec![Op::move_all_tasks(from, to)],
250                HoldSpec::frac(1.0 / 6.0),
251            )
252        })
253        .collect();
254    steps.append(&mut move_steps);
255    steps.push(Step::new(vec![], HoldSpec::frac(1.0 / 6.0)));
256
257    execute_scenario(ctx, backdrop, steps)
258}
259
260/// Disjoint cpusets cleared mid-run with cpu-controller disabled.
261///
262/// Two cgroups (`cg_0`, `cg_1`) persist across both Steps — the
263/// second Step's `Op::clear_cpuset` targets them by name. Declaring
264/// them in the [`Backdrop`] keeps their cpuset assignment alive for
265/// the first phase and lets the second Step reach the same cgroups
266/// without per-step teardown removing them.
267pub fn custom_cgroup_no_ctrl_cpuset_change(ctx: &Ctx) -> Result<AssertResult> {
268    let backdrop = Backdrop::new()
269        .push_cgroup(CgroupDef::named("cg_0").cpuset(CpusetSpec::disjoint(0, 2)))
270        .push_cgroup(CgroupDef::named("cg_1").cpuset(CpusetSpec::disjoint(1, 2)));
271
272    let steps = vec![
273        // Phase 1: hold the Backdrop's initial disjoint cpusets.
274        Step::new(vec![], ctx.settled_hold(0.5)),
275        // Phase 2: clear cpusets, hold remaining half.
276        Step::new(
277            vec![Op::clear_cpuset("cg_0"), Op::clear_cpuset("cg_1")],
278            HoldSpec::frac(0.5),
279        ),
280    ];
281
282    execute_scenario(ctx, backdrop, steps)
283}
284
285/// Heavy SpinWait vs light YieldHeavy cgroups with cpu-controller disabled.
286pub fn custom_cgroup_no_ctrl_load_imbalance(ctx: &Ctx) -> Result<AssertResult> {
287    let steps = vec![Step::with_defs(
288        vec![
289            CgroupDef::named("cg_0").workers(16),
290            CgroupDef::named("cg_1")
291                .workers(1)
292                .work_type(WorkType::YieldHeavy),
293        ],
294        ctx.settled_hold(1.0),
295    )];
296
297    execute_steps(ctx, steps)
298}
299
300/// IoSyncWrite cgroup vs fully-subscribed SpinWait cgroup.
301pub fn custom_cgroup_io_compute_imbalance(ctx: &Ctx) -> Result<AssertResult> {
302    let steps = vec![Step::with_defs(
303        vec![
304            ctx.cgroup_def("cg_0").work_type(WorkType::IoSyncWrite),
305            CgroupDef::named("cg_1").workers(ctx.topo.total_cpus()),
306        ],
307        ctx.settled_hold(1.0),
308    )];
309
310    execute_steps(ctx, steps)
311}