Skip to content

feat(sched): scheduler perf/stability baseline recorder (ADR-0022 prereq 2, PR-1) - #217

Merged
NickFlach merged 1 commit into
mainfrom
pr1/adr0022-sched-baseline
Jul 13, 2026
Merged

feat(sched): scheduler perf/stability baseline recorder (ADR-0022 prereq 2, PR-1)#217
NickFlach merged 1 commit into
mainfrom
pr1/adr0022-sched-baseline

Conversation

@NickFlach

Copy link
Copy Markdown
Collaborator

What

PR-1 of ADR-0022 prerequisite 2 — a scheduler perf/stability baseline so a future scheduler-constant change (the deferred COM2-latency fix) is measured, not blind (ADR-0016). Measurement-only: no timer HZ / SCHED_QUANTUM_TICKS / service-model change.

The design panel earned its keep

An adversarial design panel refuted the obvious metric: aggregate context switches are dominated by voluntary yield()s — every long-lived citizen busy-yields with a non-blocking recv, resetting quantum_counter thousands of times per 10 ms tick, so the timer-preemption path is nearly dead at rest. switch_count/tick is therefore both quantum-insensitive (a gate on it is vacuous) and host-throughput dependent (CI-flaky). The fix is a dedicated preempt_count incremented only at the timer-quantum-expiry reschedule in scheduler_tick (never on SYS_YIELD) — 1/quantum-paced by construction.

Instrumentation (minimal, measurement-only)

  • scheduler.c: preempt_count + scheduler_get_preempts(), and a read-only scheduler_get_fairness() (max reschedule gap, run-count spread, runnable count).
  • process.c: the dead now = 0 /* TODO */ stub in process_switch_to goes live (timer_get_ticks()), plus a per-PCB sched_picks counter — so last_scheduled/runtime_* stop being always-zero.
  • SYSINFO_SCHED sub-op (no new syscall → no ADR-0020 golden churn): one atomic line SCHED: switches=.. preempt=.. ticks=.. maxgap=.. spread=.. runnable=.., driven by a qsh sched command (argument-free) and QosVM.sched().

Report-only gate

make ci-smoke-sched (CI job scheduler-baseline, not release-gating) runs scripts/test_qos_sched.py in report-only mode: it records the tick-normalized baseline and asserts only a non-vacuous liveness floor (≥100 switches and ≥12 runnable over the window), so it reddens on a dead/wedged scheduler but arms no perf band yet.

Calibration selected PR-2's gated scalar

Empirical WSL calibration — preempt-per-1000-guest-ticks tracks 1/quantum almost exactly, host-invariantly:

SCHED_QUANTUM_TICKS preempt/1000t (idle, load) switch/1000t
5 (default) 199.5–200.0 ~520
20 49.9, 49.8 ~131
80 harness times out (system too sluggish to fill the tick window)

A 4× quantum change moves it 4× with <0.5 % variance. The follow-up arms a floor of ~100 (= q5-median × 0.5), revert-confirmed by a SCHED_QUANTUM_TICKS bump to 20 (q=80 grinds the harness itself, so 20 is the practical lever).

Verification

  • Build clean under -Werror; clang-format / cppcheck / check-api-consistency.sh / YAML all green.
  • make ci-smoke-sched passes locally (idle P=199.9, load P=200, liveness floor asserted).

🤖 Generated with Claude Code

…req 2, PR-1)

ADR-0022 defers the COM2-latency fix behind two prerequisites; prereq 1 (the
latency gate) shipped. This is prereq 2: a scheduler perf/stability baseline
so a future scheduler-constant change is MEASURED, not blind (ADR-0016).
Measurement-only — no timer HZ / SCHED_QUANTUM_TICKS / service-model change.

An adversarial design panel refuted the naive metric: aggregate context
switches are dominated by voluntary yield()s (every long-lived citizen busy-
yields with a non-blocking recv, resetting quantum_counter thousands of times
per 10 ms tick), so switch_count/tick is BOTH quantum-insensitive (a gate on
it is vacuous) AND host-throughput dependent (CI-flaky). The fix is a dedicated
preempt_count in the scheduler, incremented ONLY at the timer-quantum-expiry
reschedule in scheduler_tick (never on SYS_YIELD), which is 1/quantum-paced by
construction.

Exposure (measurement-only, minimal):
- scheduler.c: preempt_count + scheduler_get_preempts() and a read-only
  scheduler_get_fairness() (max reschedule gap, run-count spread, runnable
  count over the roster).
- process.c: the dead `now = 0 /* TODO */` timing stub in process_switch_to is
  made live (timer_get_ticks()), and a per-PCB sched_picks counter added, so
  last_scheduled/runtime_* stop being always-zero.
- SYSINFO_SCHED sub-op (no new syscall -> no ADR-0020 golden churn) emits one
  atomic line "SCHED: switches=.. preempt=.. ticks=.. maxgap=.. spread=..
  runnable=.."; qsh `sched` command (argument-free); QosVM.sched() in the host
  bridge.

Gate `make ci-smoke-sched` (CI job scheduler-baseline, NOT release-gating) runs
scripts/test_qos_sched.py in REPORT-ONLY mode: records the tick-normalized
baseline and asserts only a non-vacuous liveness floor (>=100 switches and
>=12 runnable over the window), so it reddens on a dead/wedged scheduler but
arms no perf band yet.

Empirical calibration (WSL, this session) SELECTED the follow-up's gated scalar
— preempt-per-1000-guest-ticks tracks 1/quantum almost exactly, host-invariant:
  q=5 (default): 199.5-200.0 (idle+load)   switch/1000t ~520
  q=20:          49.9, 49.8                switch/1000t ~131
  q=80:          harness times out (system too sluggish to fill the window)
So a 4x quantum change moves it 4x with <0.5% variance. The follow-up arms a
floor of ~100 (= q5-median x 0.5), revert-confirmed by a SCHED_QUANTUM_TICKS
bump to 20 (q=80 grinds the harness itself, so 20 is the practical lever).

Build clean under -Werror; format/cppcheck/api-consistency/YAML green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NickFlach
NickFlach merged commit a727227 into main Jul 13, 2026
25 checks passed
@NickFlach
NickFlach deleted the pr1/adr0022-sched-baseline branch July 13, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants