Skip to content

sched: replace the dead generation column with an enforced residency invariant#587

Merged
w1ne merged 2 commits into
mainfrom
fix/scheduler-cancel-invariant
Jul 18, 2026
Merged

sched: replace the dead generation column with an enforced residency invariant#587
w1ne merged 2 commits into
mainfrom
fix/scheduler-cancel-invariant

Conversation

@w1ne

@w1ne w1ne commented Jul 18, 2026

Copy link
Copy Markdown
Owner

The scheduler's module docs advertise a generation-based cancel — cancel_all_for bumps a per-peripheral generation, drain_due drops stale events. That mechanism does not exist: cancel_all_for is absent from the crate, and PeripheralEntry.generation is initialised to 0 and never written, so the staleness comparison is 0 != 0 forever. The column is dead plumbing threaded through schedule, drain_due, next_event_deadline, and the generation scratch buffers.

What actually bounds the event heap is two undocumented per-peripheral patterns, plus the scheduler-level identical-wake dedup added for the SYSTIMER O(n²) fix. This replaces the fiction with the truth, and adds a structural guard so the leak class cannot silently return.

Removes

The generation field, generation_scratch, refresh_generation_scratch, peripheral_generations(), is_stale, the parameter on schedule/drain_due/drain_due_into/next_event_deadline, and the three tests exercising the dead path. Dedup semantics are unchanged — generation was a constant 0 in the key.

Documents the real three-layer contract

  1. Scheduler-level identical-wake dedup via the queued index — the structural bound.
  2. Per-peripheral in-flight singleton guards (scheduled / chain_live / drain_chain_armed) — i2c, uart, spi, scb, dma.
  3. Arming tokens (arm_seq) — timer, systick, ledc.

Includes the per-peripheral mechanism table so the next author knows which pattern to follow, and states the residency caveat honestly: same-deadline re-arms collapse; different-deadline re-arms stay resident until they fire — bounded by distinct in-flight reconfigurations, not unbounded.

Adds the invariant

MAX_LIVE_EVENTS_PER_PERIPHERAL = 8 (above dma's 7 channels) with a per-index live count kept in lockstep with the heap, a debug_assert in debug builds, and SchedulerStats::{max_live_events_per_peripheral, live_event_ceiling_trips} in release.

Verification

  • riscv_jit_c3_oled_differential: byte-identical over 30,000,000 instructions
  • esp32c3 walk differential (heavy, --ignored): 10 passed · clamped full-state: 3 passed
  • esp32s3 walk differential: 2 passed under both event-scheduler and jit,event-scheduler
  • systick 4 · stm32-timer 5 · stm32-dma 2 · kw41z 6 · f103-dma 1 · f401 1 · f407-dma 3 · h563 1 · l073 1 · l476 1
  • Invariant proven to fire: unbounded_distinct_rearm_trips_the_ceiling — should panic ... ok, plus bounded-pattern and subsystem-exemption tests
  • Core lib suite 2044 passed; fmt + clippy clean; drift gate clean

w1ne added 2 commits July 18, 2026 23:09
…riant

The module docstring advertised a generation-based lazy cancel via
`cancel_all_for`, which does not exist. `PeripheralEntry.generation` was
initialised to 0 everywhere and never bumped, so `is_stale` could only
ever fire on its out-of-range branch and the generation comparison was
`0 != 0` forever. The whole column -- field, scratch, snapshot plumbing
through schedule/drain_due/next_event_deadline -- was dead weight that
read as protection it never provided.

Delete it, and document the contract that actually holds: scheduler-level
identical-wake dedup (#570), per-peripheral in-flight singleton guards,
and arm_seq arming tokens, with a table of which peripheral uses which.
Document the residency caveat honestly: the dedup key includes the
deadline, so re-arms at differing deadlines stay resident until they fire.

Codify the bound that was previously only implicit. The scheduler now
tracks live events per peripheral_idx and enforces a ceiling of 8
(MAX_LIVE_EVENTS_PER_PERIPHERAL, above dma's 7 channels): debug builds
panic via debug_assert, release builds count the breach in
SchedulerStats::live_event_ceiling_trips alongside a high-water mark.
Regression tests cover the bounded re-arm patterns and prove the ceiling
actually trips on unbounded distinct-deadline re-arming.

Dedup semantics are unchanged: generation was a constant 0 in the key.
@w1ne
w1ne merged commit 8a48c02 into main Jul 18, 2026
3 checks passed
@w1ne
w1ne deleted the fix/scheduler-cancel-invariant branch July 18, 2026 21:37
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.

1 participant