ktstr/scenario/
affinity.rs

1//! CPU affinity scenario implementations.
2
3use super::Ctx;
4use super::backdrop::Backdrop;
5use super::ops::{
6    CgroupDef, CpusetSpec, HoldSpec, Op, Step, execute_scenario, execute_scenario_with,
7};
8use crate::assert::{Assert, AssertResult};
9use crate::workload::AffinityIntent;
10use anyhow::Result;
11use std::collections::BTreeSet;
12use std::time::Duration;
13
14/// Two cgroups with worker affinities randomized four times during the
15/// run. Each randomization assigns half the available CPUs to each worker.
16///
17/// Both cgroups persist on the Backdrop so every `SetAffinity` op in
18/// the four randomization Steps finds the same live workers.
19pub fn custom_cgroup_affinity_change(ctx: &Ctx) -> Result<AssertResult> {
20    let backdrop = Backdrop::new()
21        .push_cgroup(CgroupDef::named("cg_0"))
22        .push_cgroup(CgroupDef::named("cg_1"));
23    let mut steps = vec![Step::new(vec![], ctx.settled_hold(0.2))];
24
25    // Pool the random sample across every CPU in the topology;
26    // `Op::SetAffinity` intersects with the cgroup's cpuset at apply
27    // time so each cgroup ends up sampling from its actual budget.
28    // Sample size is half the topology (clamped to at least 1).
29    let pool = ctx.topo.all_cpuset();
30    let count = (pool.len() / 2).max(1);
31    let intent = AffinityIntent::RandomSubset { from: pool, count };
32
33    for _ in 0..4 {
34        steps.push(Step::new(
35            vec![
36                Op::set_affinity("cg_0", intent.clone()),
37                Op::set_affinity("cg_1", intent.clone()),
38            ],
39            HoldSpec::frac(0.2),
40        ));
41    }
42
43    execute_scenario(ctx, backdrop, steps)
44}
45
46/// Two cgroups with all workers pinned to the same 2-CPU subset.
47/// Uses a relaxed 75% spread threshold since concentrated pinning
48/// increases work-unit spread under EEVDF.
49pub fn custom_cgroup_multicpu_pin(ctx: &Ctx) -> Result<AssertResult> {
50    let all = ctx.topo.all_cpus();
51    let pin_cpus: BTreeSet<usize> = if all.len() >= 2 {
52        all[..2].iter().copied().collect()
53    } else {
54        all.iter().copied().collect()
55    };
56
57    // Pinning all workers to 2 CPUs concentrates load and increases
58    // spread under EEVDF; relax the default spread threshold
59    // (`spread_threshold_pct`: 35% debug / 15% release).
60    let checks = Assert::default_checks().max_spread_pct(75.0);
61
62    // Settle step uses a fixed minimum so `ctx.settle = Duration::ZERO`
63    // (the production default) does not produce an instant no-op
64    // settle step. Workers spawned during Backdrop setup (cg_0 / cg_1
65    // below) need wall-clock time to migrate into their cgroup
66    // cpusets before the Op::set_affinity ops fire in the next step;
67    // without the 500ms floor a ZERO-duration settle would let the
68    // affinity-pin step race the worker dispatch path.
69    // `HoldSpec::fixed(ZERO)` itself is VALID per HoldSpec::validate
70    // (scenario/ops/types/step.rs) — the floor is for scheduler-warmup
71    // correctness, not validation.
72    let settle = ctx.settle.max(Duration::from_millis(500));
73    let backdrop = Backdrop::new()
74        .push_cgroup(CgroupDef::named("cg_0"))
75        .push_cgroup(CgroupDef::named("cg_1"));
76    let steps = vec![
77        Step::new(vec![], HoldSpec::fixed(settle)),
78        Step::new(
79            vec![
80                Op::set_affinity("cg_0", AffinityIntent::Exact(pin_cpus.clone())),
81                Op::set_affinity("cg_1", AffinityIntent::Exact(pin_cpus)),
82            ],
83            HoldSpec::fixed(ctx.duration),
84        ),
85    ];
86
87    execute_scenario_with(ctx, backdrop, steps, Some(&checks))
88}
89
90/// Two cgroups with disjoint cpusets, workers pinned to 2 CPUs within
91/// each partition. Checks pinning interacts correctly with cpuset
92/// constraints. Uses a relaxed 75% spread threshold.
93pub fn custom_cgroup_cpuset_multicpu_pin(ctx: &Ctx) -> Result<AssertResult> {
94    let usable = ctx.topo.usable_cpus();
95    let mid = usable.len() / 2;
96    let a: BTreeSet<usize> = usable[..mid].iter().copied().collect();
97    let b: BTreeSet<usize> = usable[mid..].iter().copied().collect();
98
99    let pin_a: BTreeSet<usize> = a.iter().copied().take(2.min(a.len())).collect();
100    let pin_b: BTreeSet<usize> = b.iter().copied().take(2.min(b.len())).collect();
101
102    // Pinning workers to 2 CPUs within each cpuset partition
103    // concentrates load and increases spread; relax the threshold.
104    let checks = Assert::default_checks().max_spread_pct(75.0);
105
106    let backdrop = Backdrop::new().extend_cgroups([
107        CgroupDef::named("cg_0").cpuset(CpusetSpec::disjoint(0, 2)),
108        CgroupDef::named("cg_1").cpuset(CpusetSpec::disjoint(1, 2)),
109    ]);
110    let steps = vec![
111        Step::new(vec![], HoldSpec::fixed(ctx.settle)),
112        Step::new(
113            vec![
114                Op::set_affinity("cg_0", AffinityIntent::Exact(pin_a)),
115                Op::set_affinity("cg_1", AffinityIntent::Exact(pin_b)),
116            ],
117            HoldSpec::fixed(ctx.duration),
118        ),
119    ];
120
121    execute_scenario_with(ctx, backdrop, steps, Some(&checks))
122}