Scheduler Definitions
A Scheduler tells the framework how to find, configure, and launch
the scheduler under test. declare_scheduler! builds one and
registers it so both #[ktstr_test] and the
verifier sweep can see it:
use ktstr::declare_scheduler;
use ktstr::prelude::*;
declare_scheduler!(MY_SCHED, {
name = "my_sched",
binary = "scx_my_sched",
sched_args = ["--exit-dump-len", "1048576"],
topology = (1, 2, 4, 1),
});
#[ktstr_test(scheduler = MY_SCHED)]
fn basic(ctx: &Ctx) -> Result<AssertResult> {
execute_defs(ctx, vec![ctx.cgroup_def("cg_0"), ctx.cgroup_def("cg_1")])
}
MY_SCHED is the Rust handle tests reference; name = "my_sched"
is the user-visible label in nextest output, sidecars, and the CLI.
Rename either independently. Once declared, the scheduler shows up
in the verifier sweep’s cells with no further wiring:
Nextest run ID 3522bea7-... with nextest profile: default
Starting 4 tests across 1 binary (55 tests skipped)
PASS [ 12.406s] (1/4) ktstr::kaslr_axis_e2e verifier/ktstr_sched/kernel_7_0/odd-3llc
PASS [ 12.432s] (2/4) ktstr::kaslr_axis_e2e verifier/ktstr_sched/kernel_7_0/smt-2llc
...
Defining a scheduler
declare_scheduler! emits a pub static MY_SCHED: Scheduler and
registers a reference to it in the KTSTR_SCHEDULERS distributed
slice, which is what cargo ktstr verifier enumerates.
#[ktstr_test(scheduler = ...)] expects the bare ident; the macro
takes the reference internally. The ident can carry a visibility
prefix (pub, pub(crate)).
Accepted fields
name plus exactly one binary-source key (binary, binary_path,
or the kernel_builtin_enable/kernel_builtin_disable pair) are
required; every other key is optional.
name = "..."— short human name (required).binary = "scx_name"— discover a binary by name. Resolution happens entirely on the host, before the VM boots, and the resolved binary is packed into the guest initramfs — nothing is resolved inside the guest. The cascade: a per-nameKTSTR_SCHEDULER_BIN_<NAME>env override, then the globalKTSTR_SCHEDULER, then a fresh workspace build viacargo build -p <name>— a failed build refuses to serve a possibly-stale pre-built binary unlessKTSTR_SCHEDULER_ALLOW_STALE_FALLBACKis set, which enables the pre-built fallbacks (a sibling of the test binary, thentarget/{release,debug}/). Test binaries run outside the cargo-ktstr pipeline (KTSTR_CARGO_TEST_MODE=1) skip the build and consult the hostPATHand the pre-built fallbacks first instead.binary_path = "/abs/path"— explicit pre-built binary; must exist on the host, packed into the initramfs as-is.kernel_builtin_enable = [...]+kernel_builtin_disable = [...]— paired guest shell-command lists for a scheduler compiled into the kernel (no userspace binary). Both keys must appear together.sched_args = ["--a", "--b"]— scheduler CLI args applied to every test; per-testextra_sched_argsappend after them.kargs = ["nosmt"]— extra guest kernel command line (not the scheduler’s CLI — that’ssched_args). Do not override the kargs ktstr injects itself (console=,loglevel=,rdinit=); those break guest init.sysctls = [Sysctl::new("kernel.foo", "1")]— applied at guest boot, before the scheduler starts (see below).topology = (numa_nodes, llcs, cores, threads)— default VM topology tests inherit dimension-by-dimension.constraints = TopologyConstraints { ... }— gauntlet constraints tests inherit; see the macro reference.cgroup_parent = "/path"— cgroup subtree the guest creates for the scheduler before it starts (see below).config_file = "configs/my.toml"/config_file_def = (...)— the two config-file seams (see below).assert = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0)— scheduler-level checking overrides, merged between the library defaults and per-test attributes. See Customize Checking.kernels = ["6.14", "6.15..=7.0"]— filters which kernel-list entries this scheduler verifies against in the verifier sweep. Entries use the same grammar ascargo ktstr verifier --kernel(exact versions, inclusive ranges, paths, git specs); empty means no filter. Match semantics live in BPF Verifier Sweep.
Manual definition
The const builder still works when the macro doesn’t fit — e.g. a programmatically composed scheduler, or a fixture that must stay out of the verifier sweep:
use ktstr::prelude::*;
const MITOSIS: Scheduler = Scheduler::named("scx_mitosis")
.binary(SchedulerSpec::Discover("scx_mitosis"))
.topology(1, 2, 4, 1)
.sched_args(&["--exit-dump-len", "1048576"])
.cgroup_parent("/ktstr")
.assert(Assert::NO_OVERRIDES.max_imbalance_ratio(2.0));
Scheduler::named("foo").binary_discover("scx_foo") is shorthand
for .binary(SchedulerSpec::Discover("scx_foo")) — the argument is
the binary name to discover, not the scheduler name. A manual const
is not registered in KTSTR_SCHEDULERS, so the verifier sweep does
not see it; use declare_scheduler! for anything that should
participate in cargo ktstr verifier.
SchedulerSpec
pub enum SchedulerSpec {
Eevdf, // no sched_ext binary — kernel EEVDF
Discover(&'static str), // host-side discovery by name
Path(&'static str), // explicit host path
KernelBuiltin { // compiled into the kernel
enable: &'static [&'static str],
disable: &'static [&'static str],
},
}
Scheduler::EEVDF (binary SchedulerSpec::Eevdf) runs tests under
the kernel’s default scheduler and is what #[ktstr_test] uses when
scheduler = is omitted. It is not reachable via
declare_scheduler! — reference Scheduler::EEVDF directly.
Eevdf and KernelBuiltin are excluded from the verifier sweep:
neither has a userspace binary to load BPF programs from.
Kernel-builtin example
declare_scheduler!(MINLAT, {
name = "minlat",
kernel_builtin_enable = ["echo minlat > /sys/kernel/debug/sched/ext/root/ops"],
kernel_builtin_disable = ["echo none > /sys/kernel/debug/sched/ext/root/ops"],
});
The enable commands run in the guest before scenarios start;
disable runs after they complete.
Sysctls
sysctls takes Sysctl::new("key", "value") pairs (dot-separated
keys; duplicates apply in order, last write wins). The framework
injects each as sysctl.<key>=<value> on the guest kernel command
line, so the kernel applies them at boot — each test gets a fresh
VM, so there is no apply/revert step. Sysctl::new is const fn,
so a shared tuning block can live in a const slice:
const RT_TUNING: &[Sysctl] = &[
Sysctl::new("kernel.sched_rt_runtime_us", "950000"),
Sysctl::new("kernel.numa_balancing", "0"),
];
declare_scheduler!(RT_TUNED, {
name = "rt_tuned_scx",
binary = "scx_rt_tuned",
sysctls = RT_TUNING,
});
Config files
Pick one of config_file / config_file_def — they are
alternatives.
- The config is the same file for every test →
config_file = "configs/my_sched.toml". The framework packs the host file into the guest at/include-files/{filename}and prepends--config /include-files/{filename}to the scheduler args. The--configflag name is fixed; a scheduler that uses a different flag can still take the packed path viasched_args, but must tolerate the extra--configargument. - The config varies per test →
config_file_def = ("--config={file}", "/include-files/my.json")declares the arg-template + guest-path pair, and each test supplies content via#[ktstr_test(config = …)]. The pairing is enforced both ways at compile time — see Inline scheduler config.
Both fields may technically coexist (the config_file path is
always packed and its flag prepended; the inline config is written
when a test supplies config = …), but a two-config launch is
rarely what anyone wants — pick one.
Cgroup parent
cgroup_parent = "/ktstr" makes guest init create
/sys/fs/cgroup/ktstr (enabling cpuset/cpu controllers on its
ancestors) before the scheduler starts. It does not pass
--cell-parent-cgroup to the scheduler — a cell-aware scheduler
that needs the flag must carry it in sched_args or per-test
extra_sched_args, and the guest then also creates the directory
named by the flag. Paths are validated at compile time by
CgroupPath: they must start with /, must not be / alone, and
must not contain ...
The same validation applies to any --cell-parent-cgroup value
found in sched_args / extra_sched_args at test setup: empty
values, bare /, relative paths, and a trailing flag with no value
all panic with an actionable message instead of resolving to (or
next to) the host cgroup root and corrupting host state.
Default topology
topology = (numa_nodes, llcs, cores_per_llc, threads_per_core)
sets the VM topology tests inherit. Scheduler::named() defaults to
(1, 1, 2, 1) — a minimal 2-CPU VM. Tests override individual
dimensions; unset ones still inherit:
// Inherits llcs=2, cores=4 from MITOSIS; overrides threads to 2.
#[ktstr_test(scheduler = MITOSIS, threads = 2)]
fn smt_test(ctx: &Ctx) -> Result<AssertResult> { /* ... */ }
Related test-attribute slots
Two #[ktstr_test] attributes complement the scheduler definition:
staged_schedulers = [PATH, …] packs extra scheduler binaries for
runtime swaps via Op::ReplaceScheduler / Op::AttachScheduler,
and workload_root_cgroup = "/path" roots workload cgroups
independently of the scheduler’s cgroup_parent. Both are
documented in the macro reference.
Payloads
Payload authoring — #[derive(Payload)], metric hints, include
files — lives on its own page:
Payloads and Included Files.