Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Customize Checking

Override checking thresholds for schedulers that tolerate higher imbalance, different gap thresholds, or relaxed event rates — and opt in to the checks that are off by default.

Warning

Assert::default_checks() is Assert::NO_OVERRIDES — every field None. Until a scheduler-level or per-test override sets a threshold, no worker assertions run. A green suite with no overrides proves only that the VM booted and the scheduler didn’t crash.

What a tripped gate looks like

Here a test set min_iteration_rate to a floor the workload could never meet (deliberately, to force the failure). The report names each worker that missed the gate, with the measured rate and the floor it was compared against:

    ktstr_test 'throughput_gate' [sched=scx-ktstr] [topo=1n1l2c1t] failed:
      worker 71 iteration rate 41903.3/s below floor 50000000.0/s
      worker 73 iteration rate 37834.5/s below floor 50000000.0/s

    --- stats ---
    2 workers, 4 cpus, 2 migrations, worst_spread=0.0%, worst_gap=21ms
      cg0: workers=1 cpus=2 spread=0.0% gap=10ms migrations=1 iter=209600
      cg1: workers=1 cpus=2 spread=0.0% gap=21ms migrations=1 iter=189252
...
    --- monitor ---
    samples=41 max_imbalance=2.00 max_dsq_depth=0 stuck=0
    avg: imbalance=1.32 nr_running/cpu=1.2 dsq/cpu=0.0
    events: fallback=0 (0.0/s) keep_last=210 (52.5/s) offline=0
    events+: refill_slice_dfl=210
    schedstat: csw=586 (146/s) run_delay=381246314ns/s ttwu=204 goidle=1
    bpf: ktstr_select_cp cnt=189 145ns/call
    bpf: ktstr_enqueue cnt=373 34ns/call
    bpf: ktstr_dispatch cnt=584 237ns/call
    verdict: monitor OK

Note the two channels: the worker gate tripped (the two below floor lines) while the monitor verdict is OK — worker checks and host-side monitor checks are evaluated independently. See Checking for the model. The fix is whichever of these matches the intent: set a floor the scheduler can actually meet, or fix the scheduler until it meets the floor you wrote.

Scheduler-level overrides

Declare a scheduler with assertion overrides that apply to every test using it:

use ktstr::declare_scheduler;
use ktstr::prelude::*;

declare_scheduler!(RELAXED, {
    name = "relaxed",
    binary = "scx_relaxed",
    assert = Assert::NO_OVERRIDES
        .max_imbalance_ratio(5.0)    // tolerate 5:1 imbalance
        .max_fallback_rate(500.0)    // higher fallback rate ok
        .fail_on_stall(false),       // don't fail on stall
});

These are the first layer that can carry an actual check — without them (or a per-test override), nothing asserts.

Per-test overrides

Attributes on #[ktstr_test] merge last and win:

#[ktstr_test(
    scheduler = RELAXED,
    not_starved = true,
    max_gap_ms = 5000,
    max_imbalance_ratio = 10.0,
    sustained_samples = 10,
)]
fn high_imbalance_test(ctx: &Ctx) -> Result<AssertResult> {
    // Inherits topology from RELAXED
    Ok(AssertResult::pass())
}

not_starved = true enables the starvation, fairness-spread, and scheduling-gap checks as a group; each threshold can still be overridden independently. The full attribute list and default thresholds live in the #[ktstr_test] reference.

Merge order

The runtime evaluates Assert::default_checks().merge(&scheduler.assert).merge(&test.assert) — three layers, last-Some-wins per field. Worked example:

  • scheduler layer: max_imbalance_ratio(5.0)
  • test layer: max_imbalance_ratio = 10.0
  • effective: 10.0 — the test’s Some wins; every field the test leaves None falls through to the scheduler layer, then to the (all-None) defaults.

To see the merged result for a registered test without reading source:

cargo ktstr show-thresholds high_imbalance_test

It prints every threshold field of the exact Assert the runtime will evaluate, with none for unset fields.

Using Assert directly in ops scenarios

fn my_scenario(ctx: &Ctx) -> Result<AssertResult> {
    let checks = Assert::NO_OVERRIDES
        .check_not_starved()
        .max_gap_ms(3000);

    let steps = vec![/* ... */];
    execute_steps_with(ctx, steps, Some(&checks))
}

execute_steps_with applies the given Assert for worker checks, overriding the merged config. execute_steps (without _with) passes None and falls back to ctx.assert — the merged three-layer config above. Reaching for _with when you meant to add to the merged config is a classic trap: the explicit Assert replaces ctx.assert, it does not compose with it.

See Ops, Steps, and Backdrop for the step execution model.