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
//! End-to-end proof that schbench's `--split` mode runs inside a real VM. Boots
//! with scx-ktstr, runs a backdrop schbench workload configured with
//! `split_percent(Some(50))` and two internal worker threads, holds one phase,
//! and reads the per-phase wakeup/request percentiles back in post_vm.
//!
//! `--split` is the deliberate-cache-contention mode: each cycle runs some matrix
//! multiplies on ONE process-global shared matrix (modeled with `AtomicU64`
//! `Relaxed` load/store, so the shared race is sound) and the rest on a per-worker
//! private matrix (schbench.c:1390-1404). Two worker threads concurrently
//! multiply into the shared matrix (per-k stores to the shared C cells), so this
//! exercises the real concurrent shared path in a guest. The gate is the FRAMEWORK INVARIANT: the split workload boots,
//! runs, and emits its per-phase latency metrics -- it does NOT gate on absolute
//! latency (scx-ktstr is a fixture, not a performance scheduler). A deadlock,
//! panic, or data-race trap in the shared-matrix path would produce no metric and
//! fail Gate 1.
use anyhow::Result;
use ktstr::assert::{AssertResult, Phase};
use ktstr::ktstr_test;
use ktstr::prelude::VmResult;
use ktstr::scenario::Ctx;
use ktstr::test_support::{Scheduler, SchedulerSpec};
const SPLIT_SCX: Scheduler =
Scheduler::named("split_scx").binary(SchedulerSpec::Discover("scx-ktstr"));
/// The two per-phase percentiles the split run must emit (both LowerBetter
/// latencies). Their presence proves the split engine ran end-to-end in the VM
/// and recorded both the wakeup (message-handshake) and request latencies.
const WAKEUP_METRIC: &str = "wakeup_p99_latency_us";
const REQUEST_METRIC: &str = "request_p99_latency_us";
/// post_vm: the framework-invariant gate. The split workload produced both
/// per-phase percentiles -- proves the shared-matrix concurrent path ran to
/// completion in the guest (no deadlock, no panic, no data-race trap).
fn assert_split_workload_ran(result: &VmResult) -> Result<()> {
let split = Phase::step(0); // the lone hold step (step_index 1)
anyhow::ensure!(
result.phase_metric(split, WAKEUP_METRIC).is_some(),
"split phase produced no {WAKEUP_METRIC} -- the split workload did not record latencies"
);
anyhow::ensure!(
result.phase_metric(split, REQUEST_METRIC).is_some(),
"split phase produced no {REQUEST_METRIC} -- the split workload did not record latencies"
);
Ok(())
}
// Single 6s hold step; duration_s covers it with margin and watchdog_timeout_s
// lifts the VM hard deadline to cover boot + the hold (vm_timeout_from_entry,
// test_support/runtime.rs).
#[ktstr_test(
scheduler = SPLIT_SCX,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 8,
watchdog_timeout_s = 30,
cleanup_budget_ms = 5000,
num_snapshots = 3,
post_vm = assert_split_workload_ran,
)]
fn schbench_split_runs_in_vm(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::backdrop::Backdrop;
use ktstr::scenario::ops::{CgroupDef, HoldSpec, Step, execute_scenario};
use ktstr::workload::{SchbenchConfig, WorkType};
use std::time::Duration;
// split_percent(Some(50)): half the cache footprint is the per-thread private
// matrix, half is the ONE shared matrix every worker contends on. Two worker
// threads make the shared-matrix writes genuinely concurrent. WorkType::Schbench
// should use a single ktstr worker (the message/worker topology is internal).
let cfg = SchbenchConfig::default()
.worker_threads(2)
.split_percent(Some(50));
let backdrop = Backdrop::new().push_cgroup(
CgroupDef::named("split_bench")
.work_type(WorkType::schbench(cfg))
.workers(1),
);
let steps = vec![Step::new(vec![], HoldSpec::fixed(Duration::from_secs(6)))];
execute_scenario(ctx, backdrop, steps)
}