C3 walk-free: bus cycle clock (read-side freshness) + RTC lazy migration + write-choke IRQ aggregation#516
Merged
Merged
Conversation
…e-choke IRQ aggregation
Walk-free plan Part 1 lands as shared infrastructure, with the ESP32-C3
RTC main timer as its first production user and the C3 interrupt
aggregation made batched-interval-compatible (perf/c3-walk-free).
Task A — bus-published CycleClock (read-side freshness):
- New `cycle_clock` module: an Arc<AtomicU64> the bus publishes
`current_cycle` into at every refresh point (batch start/end, step,
idle fast-forward) via the new `SystemBus::set_current_cycle` choke.
A `&self` MMIO read can lazily sync Cell-held counter state to it —
exact at batch boundaries, < one peripheral_tick_interval stale
mid-batch, the identical bound the write-path sync_to documents.
- `Peripheral::attach_cycle_clock` (default no-op) hands every
peripheral a clone at the `add_peripheral` attach choke. Without an
attached clock a model must stay on its legacy walk path, so
hand-built buses keep exact semantics.
Task B — esp32c3/rtc_timer.rs off the walk:
- `uses_scheduler()` flips true when the clock is attached under
event-scheduler; the per-cycle walk skips the RTC entirely. The
TIME_UPDATE latch (the only point firmware observes time) syncs from
the published clock; the write-path sync_to shares the same anchored
advance, so double-counting is impossible by construction.
- The dead-anchor double-count trap flagged by the timing audit is
gone: tick_elapsed no longer bumps anchor_tick (and is inert in
scheduler mode); the anchor belongs exclusively to the lazy path.
- runtime_snapshot still persists anchor_tick, but restore re-anchors
to the live clock: a snapshot resumed on a fresh machine (cycles
~0) previously risked freezing the counter until "now" caught up
with a 150M-cycle stale anchor — the exact spin-forever failure the
model exists to prevent. Counter value (boot-log determinism)
carries across unchanged.
- Legacy path preserved byte-identically for feature-off builds and
clockless buses.
Task C — C3 IRQ aggregation vs the fast paths:
- aggregate_esp32c3_irqs now records the tick's asserting source bitmap
and rebuilds the routed line mask through a single shared body
(recompute_esp32c3_irq_lines). INTC config + FROM_CPU IPI writes
re-aggregate AT THE WRITE via sync_esp32c3_irq_cache_write, so at
interval > 1 a yield/critical-section transition is reflected at the
write instruction instead of up to one interval late; at interval 1
the tick-end rebuild runs before the CPU's next interrupt check, so
behaviour is byte-identical. Peripheral source assert/de-assert stays
tick-quantised (≤ one interval — the write-path bound).
- per_cycle_tick_is_trivial no longer excludes C3 buses when the cached
aggregation is present: on a (future) walk-deleted C3 bus nothing
ticks, and the write chokes keep FROM_CPU/INTC routing correct, so
the trivial per-cycle early-out is sound.
- The inherent SystemBus::write_u32/write_u16 paths now sync the C3 IRQ
cache like the Bus-trait accessors already did (pre-existing
coherence gap, exposed by the new write-choke test).
- Removed the per-read `std::env::var("LABWIRED_RXBUF_TRACE")` syscall
from Bus::read_u32 (every load instruction!) and from the aggregation
tick path — now a process-cached OnceLock gate in wifi_mac.
Gates (all green; #511/#512 style):
- tests/esp32c3_walk_differential.rs: the REAL esp32c3-oled-demo lab
(browser flash fast-start assembly) walk-on RTC vs scheduler RTC at
interval 1 — serial, total_cycles and SSD1306 framebuffer
byte-identical after 30M instructions; framebuffer additionally
byte-identical across intervals {1, 64}; FROM_CPU/INTC write-choke
re-aggregation asserted with no tick in between; endgame ledger
pins that rtc_cntl_timer no longer pins the walk and
max_safe_tick_interval honestly stays 1 (systimer et al. remain).
- src/tests/esp32c3_rtc_delay_loop.rs: the firmware busy-poll delay
loop the old comment feared, proven terminating and byte-identical
to the walk at interval 1 AND 64.
- Native throughput probe (50M instructions, Machine::run, release,
median of 3): OLED lab at interval 1 goes 3.27 -> 4.56 MIPS (+39%);
interval 64 reaches 15.5 MIPS (the unlock target once the remaining
walkers migrate).
…perf/c3-walk-free
…on + regen status
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First production user of the walk-free plan's read-side freshness mechanism, plus the two C3-specific unlocks. Measured: ESP32-C3 OLED lab 3.27 → 4.56 MIPS native at interval 1 (+39%); 15.5 MIPS at interval 64 (unreachable until the remaining systimer/Class-A migrations land — max_safe_tick_interval honestly stays 1 for C3, asserted in a committed ledger test).
cycle_clock.rs): bus-publishedArc<AtomicU64>"now", written only bySystemBus::set_current_cycle(the single choke covering batch start/end, step, idle-FF), handed to peripherals via newPeripheral::attach_cycle_clockat add-time. Contract: exact at batch boundaries, < one interval stale mid-batch — identical to the write-pathsync_tobound. No clock attached ⇒ model must stay on the walk (conservative).advance_todriven by both write-path sync and the TIME_UPDATE latch pullingclock.now()(its only firmware-visible time is the write-latch — hardware semantics). Fixes the audited double-count trap (tick_elapsedno longer bumps the anchor; modes mutually exclusive by construction). Resume re-anchors to the live clock so a snapshot restored on a fresh machine cannot freeze the counter (unit-tested).per_cycle_tick_is_trivialno longer excludes C3. At interval 1 byte-identical; at interval > 1 write-driven inputs deliver at the write instruction, peripheral sources tick-quantised ≤ one interval. Also fixes a pre-existing coherence gap: the inherentSystemBus::write_u32/u16never synced the C3 IRQ cache.LABWIRED_RXBUF_TRACEwas anenv::varon every load instruction — now a process-cached OnceLock gate.Differentials (committed): real OLED-demo lab via the browser-equivalent flash-fast-start assembly, walk-on vs scheduler RTC — serial + total_cycles + SSD1306 framebuffer byte-identical over 30M instructions, framebuffer byte-identical across intervals {1,64}; the feared rtc_time_get busy-poll delay loop terminates byte-identically on both paths at intervals 1 and 64.
All lanes green (workspace lib, jit+event-scheduler 1845, clippy -D warnings both, fmt, wasm32 check, leo/oled e2e incl. the wasm fast-start paint test); known machine-local failures verified identical on untouched main.
Deliberately deferred (honest ledger in-tree): systimer event migration (needs the C3-matrix routing hook in apply_event_result), C3 I2C (wire-engine class), C3 Class-A sweep.