rx.parse_abort: count the abandoned-aggregate RX loss on every generation - #387
rx.parse_abort: count the abandoned-aggregate RX loss on every generation#387josephnef wants to merge 1 commit into
Conversation
…tion An RX descriptor walk that hits a malformed/truncated descriptor mid-aggregate abandons every remaining frame in that bulk-IN buffer. Those frames were already admitted by the chip — and, with an ACK responder armed, already ACKed to the peer — so this loss class is post-admission: invisible to a hardware-ARQ transmitter, which counts the frames delivered and never retries. It was also invisible to us: Jaguar2/Jaguar3/Kestrel broke out of the walk silently, and Jaguar1 only warned on the diagnostic plane. One shared helper (src/RxParseAbort.h) now emits a machine event at all four sites, with normal end-of-aggregate zero padding (all-zero remainder) excluded so the event only fires on real aborts. The counter is cumulative per RX loop; absence of the event in a session means the walk never aborted. Hardware-validated on all four generations (tests/parse_abort_smoke.sh: ambient-RX per die — frames flow, zero spurious aborts on each family's aggregate padding format), plus a 3-arm ARQ e2e campaign on the 8822EU (~1.14M delivered frames incl. 685k inside A-MPDU aggregates, zero events) — so the exclusion heuristic is proven non-flooding at scale. The smoke's J3 default DUT is the 8812CU: the bench 8822EU decodes no ambient 2.4 GHz (front-end quirk, 5 GHz proven) and would fail the smoke for an unrelated reason. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by QodoEmit rx.parse_abort for abandoned RX aggregates across generations
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1. rx.parse_abort docs duplicated
|
| | `rx.corrupt` | RX (`DEVOURER_RX_DUMP_ALL`) | len, crc, icv, rate, bw, stbc, ldpc, sgi, rssi[2], evm[2], snr[2] | | ||
| | `rx.txhit` | RX, TX | hits, total_rx, len, seq, paggr, ppdu, rate, bw, stbc, ldpc, ppdu_type — canonical-SA (57:42:75:05:d6:00) matcher; rate/ldpc prove what encoding was decoded (8814A reports ldpc=0 always — no HW indicator); ppdu_type is the AX RXD format nibble (7=HE_SU, 8=HE_ERSU; 255 on pre-AX chips) | | ||
| | `rx.seq` | RX, duplex (`DEVOURER_RX_PCTR`) | pctr, tsfl, seq, crc, paggr, ppdu, rate (hw rate index of this copy — retransmissions of one pctr can air at different rates, so per-copy rate + tsfl order reconstructs the fw fallback ladder on air, `tests/retry_ladder_probe.sh`) — the ground-truth per-frame delivery sequence for the RX-ring loss study: pctr is the u32 the txdemo QoS-Data path stamps at MPDU offset 26, so gaps in it are per-frame loss; paggr/ppdu carry the aggregate structure the host-vs-RF discriminator keys on. Lean by design (no body hex) so the emit can't perturb the pump thread. SA gate follows `DEVOURER_RX_AGG_SA` (required in duplex, whose canonical-SA `rx.frame` stream is a different transmitter), else canonical SA | | ||
| | `rx.parse_abort` | RX (every generation, always on) | t, off, buf_len, remaining, frame_len, drvinfo, shift, total (cumulative) — the RX descriptor walk hit a malformed/truncated descriptor mid-aggregate and abandoned the rest of the bulk-IN buffer: every abandoned frame was already admitted by the chip (and, with an ACK responder armed, already ACKed to the peer), so this is post-admission loss a hardware-ARQ peer counts as delivered. Normal end-of-aggregate zero padding (all-zero remainder) is excluded. No event in a session = the walk never aborted | |
There was a problem hiding this comment.
1. rx.parse_abort docs duplicated 📘 Rule violation ⚙ Maintainability
The new rx.parse_abort entry in docs/logging.md repeats the same explanatory narrative that is already present in the new header doc-comment for src/RxParseAbort.h. Duplicating the same description in two places risks drift and violates the non-redundant documentation rule.
Agent Prompt
## Issue description
`rx.parse_abort` is described in both `docs/logging.md` and the header doc-comment in `src/RxParseAbort.h`, creating duplicated documentation that can drift.
## Issue Context
Compliance requires that documentation not restate content already documented in header doc-comments; secondary docs should point to the authoritative header comment instead.
## Fix Focus Areas
- docs/logging.md[94-94]
- src/RxParseAbort.h[4-10]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| .f("drvinfo", drvinfo) | ||
| .f("shift", shift) | ||
| .f("total", ++total); | ||
| return true; |
There was a problem hiding this comment.
2. Abort total increments silently 🐞 Bug ≡ Correctness
emit_rx_parse_abort() increments total via ++total inside the .f() argument list, so total is incremented even if the EventSink is disabled and no event is emitted; it also returns true in that case, contradicting its own “event emitted” return contract. This can make later rx.parse_abort totals disagree with the number of emitted events when events are disabled and later re-enabled.
Agent Prompt
### Issue description
`emit_rx_parse_abort()` is documented to increment `total` only when an event is emitted and to return `true` only when an event is emitted. Today it does `++total` inside `.f("total", ++total)`, so the increment happens even if the sink is disabled (no emission). It also returns `true` even when no emission occurs.
### Issue Context
`Ev` short-circuits when `EventSink::enabled()` is false, so no output happens, but C++ still evaluates `++total` before calling `.f()`.
### Fix Focus Areas
- src/RxParseAbort.h[19-45]
### Suggested fix
1. Early-exit when `!sink.enabled()` to avoid both the scan and any counter changes.
2. Move the increment to a separate statement executed only on the emission path:
```cpp
if (!sink.enabled())
return false;
...
if (all_zero)
return false;
++total;
Ev(sink, "rx.parse_abort")... .f("total", total);
return true;
```
This restores the stated contract for both `total` and the return value.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
What
A machine event for a loss class that was structurally silent: when an RX descriptor walk hits a malformed/truncated descriptor mid-aggregate, it abandons every remaining frame in that bulk-IN buffer. Those frames were already admitted by the chip — and, with an ACK responder armed, already ACKed to the peer — so a hardware-ARQ transmitter counts them delivered and never retries. Jaguar2/Jaguar3/Kestrel broke out of the walk with no trace at all; Jaguar1 warned on stderr only.
One shared helper (
src/RxParseAbort.h) now emitsrx.parse_abort(off / buf_len / remaining / frame_len / drvinfo / shift / cumulative total) at all four walk sites. Normal end-of-aggregate zero padding (all-zero remainder) is excluded, so the event only fires on real aborts — no event in a session means the walk never abandoned anything. Schema indocs/logging.md.Why now
A PixelPilot field investigation (8822E pair, hardware ARQ, FEC 1/1) surfaced monitor-correlated ACK-then-drop losses. The parse-walk abort was the one remaining silent path in devourer's RX chain that could produce that signature. Instrumenting it was the falsifier.
Measured
tests/arq_e2e_delivery.sh, 8822EU DUT, ch161, retry 4, spsc-fat+backpressure, video-shaped 1400 B and aggregating 512 B loads) — ~1.14 M delivered frames, 685 k inside A-MPDU aggregates: 0 parse aborts, 0 ACKed-but-undelivered attributable to the RX chain. The adversarial pair: the A-MPDU arm's verdict line flagged au=2, but the witness ledger shows both frames aired 5× and 4× with rate fallback and were never BlockAcked — CCX false-oks under AGG_EN (the documented A-MPDU accounting collapse), not delivery loss.tests/parse_abort_smoke.sh(new; ambient-RX per generation, verdict = frames flow ∧ zero events) — 8814AU (J1), 8822BU (J2), 8812CU (J3, post-refactor walk), 35bc:0101 (Kestrel) all OK. The zero-abort result also means the event's cost on healthy links is zero emissions; the all-zero scan only runs on the walk's exit path.🤖 Generated with Claude Code