1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! End-to-end coverage for per-CPU BPF map projection:
//! [`BpfMapProjector::field_cpu_sum_u64`] (cross-CPU aggregate) and
//! [`BpfMapProjector::cpu`] → [`BpfMapCpuProjector::field_u64`]
//! (per-CPU select), exercised against a REAL `BPF_MAP_TYPE_PERCPU_ARRAY`
//! (scx-ktstr's `ktstr_pcpu_fix`) captured from a booted scheduler.
//!
//! No prior test captured a real per-CPU map (`ktstr_pcpu_fix`, the
//! only `BPF_MAP_TYPE_PERCPU_ARRAY` in scx-ktstr, was added for this
//! test — see its decl comment in `scx-ktstr/src/bpf/main.bpf.c`), so
//! this validates the
//! whole per-CPU capture → render → projector chain end-to-end, not
//! just the host-side projector logic the bpf.rs chain tests cover with
//! synthetic data.
//!
//! scx-ktstr stamps each dispatching CPU's own slot with
//! `KTSTR_PERCPU_MAGIC + cpu_id` from `ktstr_dispatch` (via
//! `bpf_map_lookup_elem`, which returns the running CPU's slot). After a
//! workload across a 2-core VM:
//! * the per-CPU SUM of `magic` peaks at >= MAGIC in some capture
//! (at least one CPU dispatched and stamped its slot), and
//! * per-CPU SELECT reads back `MAGIC + n` for a CPU `n` that ran.
//!
//! Vacuity: it does NOT pin WHICH CPUs ran or the exact sum (the
//! scheduler picks placement); it pins that a real per-CPU map's
//! captured slots flow through both projector paths with the
//! BPF-stamped values.
use anyhow::Result;
use ktstr::assert::AssertResult;
use ktstr::ktstr_test;
use ktstr::prelude::VmResult;
use ktstr::scenario::Ctx;
use ktstr::scenario::ops::{CgroupDef, HoldSpec, Step, execute_steps};
use ktstr::test_support::{Scheduler, SchedulerSpec};
const KTSTR_SCHED: Scheduler =
Scheduler::named("ktstr_sched").binary(SchedulerSpec::Discover("scx-ktstr"));
/// Mirrors `KTSTR_PERCPU_MAGIC` in `scx-ktstr/src/bpf/main.bpf.c`.
const KTSTR_PERCPU_MAGIC: u64 = 0x5A5A_0000_0000;
/// VM core count (matches the `cores` attr below). CPUs `0..CORES` may
/// have dispatched and stamped `ktstr_pcpu_fix`.
const CORES: usize = 2;
fn assert_percpu_projection(result: &VmResult) -> Result<()> {
anyhow::ensure!(
result.periodic_fired >= 1,
"periodic_fired = {} of {} — no capture fired; per-CPU projection \
cannot be exercised without at least one periodic sample",
result.periodic_fired,
result.periodic_target,
);
let series = result.periodic_series();
// Cross-CPU SUM: at least one CPU dispatched and stamped its slot
// with MAGIC + its id, so the per-CPU sum of `magic` peaks at
// >= MAGIC in at least one captured sample. Reading a value at all
// proves the PERCPU_ARRAY was captured + rendered + projected
// end-to-end (the bpf.rs chain tests cover the reduction logic with
// synthetic data; this proves the real capture path feeds it).
let sum_samples: Vec<u64> = series
.bpf_map("ktstr_pcpu_fix")
.at(0)
.field_cpu_sum_u64("magic")
.values_iter()
.filter_map(|r| r.as_ref().ok().copied())
.collect();
anyhow::ensure!(
!sum_samples.is_empty(),
"field_cpu_sum_u64 produced no readable sample for ktstr_pcpu_fix — \
the per-CPU map was not captured/projected end-to-end (map missing \
from the snapshot, or every per-CPU slot unreadable)",
);
let peak_sum = sum_samples.iter().copied().max().unwrap_or(0);
anyhow::ensure!(
peak_sum >= KTSTR_PERCPU_MAGIC,
"per-CPU sum of `magic` peaked at {peak_sum}, expected >= {KTSTR_PERCPU_MAGIC} \
— at least one CPU should have dispatched and stamped MAGIC + its id; \
a peak below MAGIC means no CPU's stamp was captured",
);
// Per-CPU SELECT: a CPU that dispatched reads back MAGIC + its id
// via .cpu(n).field_u64. At least one CPU in 0..CORES must have run
// the workload, so at least one select resolves the stamped value.
let mut selected = None;
for n in 0..CORES {
let vals: Vec<u64> = series
.bpf_map("ktstr_pcpu_fix")
.cpu(n)
.field_u64("magic")
.values_iter()
.filter_map(|r| r.as_ref().ok().copied())
.collect();
if vals.iter().any(|&v| v == KTSTR_PERCPU_MAGIC + n as u64) {
selected = Some(n);
break;
}
}
anyhow::ensure!(
selected.is_some(),
"no CPU in 0..{CORES} read back MAGIC + its id via \
.cpu(n).field_u64(\"magic\") — per-CPU select did not resolve a \
populated slot (the select narrow is not reading the stamped \
per-CPU value)",
);
Ok(())
}
/// Boots scx-ktstr on a 2-core VM with a multi-worker workload so both
/// CPUs dispatch (stamping `ktstr_pcpu_fix`), with periodic captures so
/// the host snapshots the populated per-CPU map.
#[ktstr_test(
scheduler = KTSTR_SCHED,
llcs = 1,
cores = 2,
threads = 1,
duration_s = 10,
watchdog_timeout_s = 20,
num_snapshots = 4,
auto_repro = false,
post_vm = assert_percpu_projection,
)]
fn percpu_projection_reads_real_percpu_array_e2e(ctx: &Ctx) -> Result<AssertResult> {
let steps = vec![Step {
setup: vec![CgroupDef::named("cg_pcpu").workers(4)].into(),
ops: vec![],
hold: HoldSpec::FULL,
}];
execute_steps(ctx, steps)
}