Skip to content

Commit 4299579

Browse files
josephnefclaude
andcommitted
rx.parse_abort: count the abandoned-aggregate RX loss on every generation
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>
1 parent 5a5dd62 commit 4299579

8 files changed

Lines changed: 150 additions & 5 deletions

File tree

‎docs/logging.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
9191
| `rx.corrupt` | RX (`DEVOURER_RX_DUMP_ALL`) | len, crc, icv, rate, bw, stbc, ldpc, sgi, rssi[2], evm[2], snr[2] |
9292
| `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) |
9393
| `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 |
94+
| `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 |
9495
| `rx.ring` | L (`DEVOURER_RX_RING_MS`) | t, mode ("async"/"sync"/"reorder-pool"/"spsc-fat"), n_urbs, armed (URBs posted to the HCD and awaiting a frame — the depth that starves under a slow inline consumer), min_armed (low-water mark since the last emit), cb_max_us (worst inline-consume latency in the window), resubmit_fail, completions (cumulative URB callbacks), empties (cumulative callbacks that left the ring with zero posted URBs), pool_free (−1 = no host pool), qdepth (spsc-fat consumer-queue depth; 0 in the other ring modes), pool_dropped (cumulative received frames discarded at spsc-fat pool exhaustion under the `drop` policy — pool exhausted, or a failed re-arm, the latter also ticking resubmit_fail; each was already chip-ACKed, so a hardware-ARQ peer counts it delivered), pool_stalls (the `backpressure`-policy counterpart, cumulative URB park events: the payload still reaches the consumer, the ring shrinks and the chip declines further ACKs, so overload loss stays ARQ-visible — `DEVOURER_RX_POOL_EXHAUST`, default backpressure; `tests/arq_e2e_delivery.sh` measures both). Sync mode emits a reduced line (pool_free pinned at −1; no qdepth/pool_dropped/pool_stalls/completions/empties). The mechanism-proof telemetry: empties/completions is the host-starvation rate — near-0 under RF loss (the ring stays armed because frames don't arrive), high under host starvation (frames out-race resubmit and drain the ring). Counted in the callback, so robust to the pump-thread starvation that makes the periodic emit sparse — but blind while the pump itself is frozen: a stalled consumer drops frames these counters never see, which the per-frame `rx.seq` ledger exists to catch |
9596
| `rx.count` | TX (its RX thread) | total, len |
9697
| `rx.path` | RX (`DEVOURER_RX_ALLPATHS`) | seq, rssi[4], snr[4], evm[4] |

‎src/RxParseAbort.h‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#ifndef DEVOURER_RX_PARSE_ABORT_H
2+
#define DEVOURER_RX_PARSE_ABORT_H
3+
4+
/* rx.parse_abort — the RX descriptor walk hit a malformed/truncated
5+
* descriptor mid-aggregate and abandoned the rest of the bulk-IN buffer.
6+
* Every abandoned frame was already admitted by the chip (and, with an ACK
7+
* responder armed, already ACKed to the peer), so this is post-admission
8+
* loss a hardware-ARQ peer counts as delivered — it must never be silent.
9+
* Normal end-of-aggregate zero padding (all-zero remainder) is excluded.
10+
* Shared by every generation's RX walk; schema: docs/logging.md. */
11+
12+
#include <cstddef>
13+
#include <cstdint>
14+
15+
#include "Event.h"
16+
17+
namespace devourer {
18+
19+
/* Returns true when the remainder was a real abort (event emitted),
20+
* false for benign all-zero padding. `total` is the caller's cumulative
21+
* abort counter, incremented on emit. */
22+
inline bool emit_rx_parse_abort(EventSink &sink, const uint8_t *rem,
23+
size_t rem_len, long long off,
24+
long long buf_len, long long frame_len,
25+
long long drvinfo, long long shift,
26+
long long &total) {
27+
bool all_zero = true;
28+
for (size_t i = 0; i < rem_len; ++i)
29+
if (rem[i] != 0) {
30+
all_zero = false;
31+
break;
32+
}
33+
if (all_zero)
34+
return false;
35+
Ev(sink, "rx.parse_abort")
36+
.t()
37+
.f("off", off)
38+
.f("buf_len", buf_len)
39+
.f("remaining", static_cast<long long>(rem_len))
40+
.f("frame_len", frame_len)
41+
.f("drvinfo", drvinfo)
42+
.f("shift", shift)
43+
.f("total", ++total);
44+
return true;
45+
}
46+
47+
} // namespace devourer
48+
49+
#endif /* DEVOURER_RX_PARSE_ABORT_H */

‎src/jaguar1/FrameParser.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "FrameParser.h"
22

3+
#include "RxParseAbort.h" /* rx.parse_abort — abandoned-aggregate event */
4+
35
#define CONFIG_USB_RX_AGGREGATION 1
46

57
#define RXDESC_SIZE 24
@@ -182,6 +184,11 @@ std::vector<Packet> FrameParser::recvbuf2recvframe(std::span<uint8_t> ptr) {
182184
"RX Warning!,pkt_len <= 0 or pkt_offset > transfer_len; pkt_len: "
183185
"{}, pkt_offset: {}, transfer_len: {}",
184186
pattrib.pkt_len, pkt_offset, pbuf.size());
187+
devourer::emit_rx_parse_abort(
188+
_logger->events(), pbuf.data(), pbuf.size(),
189+
static_cast<long long>(pbuf.data() - ptr.data()),
190+
static_cast<long long>(ptr.size()), pattrib.pkt_len,
191+
pattrib.drvinfo_sz, pattrib.shift_sz, _parse_aborts);
185192
break;
186193
}
187194

‎src/jaguar1/FrameParser.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ enum _PUBLIC_ACTION
235235
class FrameParser
236236
{
237237
Logger_t _logger;
238+
long long _parse_aborts = 0; /* cumulative rx.parse_abort count */
238239

239240
public:
240241
FrameParser(Logger_t logger);

‎src/jaguar2/RtlJaguar2Device.cpp‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "RadiotapPeek.h"
1818
#include "RadiotapTxFlags.h" /* HT MCS field decoder (LDPC/STBC) */
1919
#include "TxAggPlan.h"
20+
#include "RxParseAbort.h" /* rx.parse_abort — abandoned-aggregate event */
2021
#include "TxReport.h"
2122

2223
#include "BeamformingSounder.h"
@@ -548,15 +549,22 @@ void RtlJaguar2Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
548549
/* RX loop: async bulk-IN URB queue; walk the aggregated 8822B RX descriptors
549550
* per completion and hand each PSDU to the packet processor. */
550551
uint64_t frames = 0, reads = 0;
552+
long long parse_aborts = 0;
551553
auto on_data = [&](const uint8_t *data, int n) {
552554
cfo_tick();
553555
if (++reads <= 8)
554556
_logger->info("Jaguar2 RX: completion #{} -> {} bytes", reads, n);
555557
uint32_t off = 0;
556558
while (off + jaguar2::RXDESC_SIZE_8822B <= static_cast<uint32_t>(n)) {
557-
jaguar2::Rx8822bFrame f;
558-
if (!jaguar2::parse_rx_8822b(data + off, static_cast<size_t>(n) - off, f))
559+
jaguar2::Rx8822bFrame f{};
560+
if (!jaguar2::parse_rx_8822b(data + off, static_cast<size_t>(n) - off,
561+
f)) {
562+
devourer::emit_rx_parse_abort(_logger->events(), data + off,
563+
static_cast<size_t>(n) - off, off, n,
564+
f.frame_len, f.drvinfo_size, f.shift,
565+
parse_aborts);
559566
break;
567+
}
560568
if (_packetProcessor) {
561569
Packet p{};
562570
p.RxAtrib.pkt_len = static_cast<uint16_t>(f.frame_len);

‎src/jaguar3/RtlJaguar3Device.cpp‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "RadiotapPeek.h" /* send_packets batch pre-parse */
1313
#include "RadiotapTxFlags.h" /* HT MCS field decoder (LDPC/STBC) */
1414
#include "TxAggPlan.h" /* USB TX aggregation URB packing */
15+
#include "RxParseAbort.h" /* rx.parse_abort — abandoned-aggregate event */
1516
#include "TxReport.h" /* CCX TX-status report decode + tx.report event */
1617

1718
#include "BeamformingSounder.h" /* generation-neutral BF self-sounding recipe */
@@ -274,15 +275,22 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
274275
static_cast<int>(avg_khz), _xtal_cap);
275276
};
276277
/* Process one bulk-IN completion: walk the aggregated 8822C RX descriptors. */
278+
long long parse_aborts = 0;
277279
auto on_data = [&](const uint8_t *data, int n) {
278280
cfo_tick();
279281
if (++reads <= 8)
280282
_logger->info("Jaguar3 RX: async completion #{} -> {} bytes", reads, n);
281283
uint32_t off = 0;
282284
while (off + jaguar3::RXDESC_SIZE_8822C <= static_cast<uint32_t>(n)) {
283-
jaguar3::Rx8822cFrame f;
284-
if (!jaguar3::parse_rx_8822c(data + off, static_cast<size_t>(n) - off, f))
285+
jaguar3::Rx8822cFrame f{};
286+
if (!jaguar3::parse_rx_8822c(data + off, static_cast<size_t>(n) - off,
287+
f)) {
288+
devourer::emit_rx_parse_abort(_logger->events(), data + off,
289+
static_cast<size_t>(n) - off, off, n,
290+
f.frame_len, f.drvinfo_size, f.shift,
291+
parse_aborts);
285292
break;
293+
}
286294
if (_packetProcessor) {
287295
Packet p{};
288296
p.RxAtrib.pkt_len = static_cast<uint16_t>(f.frame_len);

‎src/kestrel/RtlKestrelDevice.cpp‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "KestrelLe.h"
1212
#include "RadiotapBuilder.h" /* build_stream_radiotap — host-injected trigger */
1313
#include "RadiotapPeek.h"
14+
#include "RxParseAbort.h" /* rx.parse_abort — abandoned-aggregate event */
1415
#include "RateDefinitions.h" /* MGN_* rate enum */
1516
#include "MacRegAx.h"
1617
#include "SignalStop.h" /* g_devourer_should_stop — set by demo signal handlers */
@@ -320,15 +321,21 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
320321
* divergence between the dies. */
321322
const uint16_t drv_info_unit =
322323
_variant == kestrel::ChipVariant::C8852C ? 16 : 8;
324+
long long parse_aborts = 0;
323325
_device.bulk_read_async_loop(
324326
32768, 8,
325327
[&, drv_info_unit](const uint8_t *data, int n) {
326328
uint32_t off = 0;
327329
while (off + 16 <= static_cast<uint32_t>(n)) {
328330
kestrel::KestrelRxFrame f;
329331
if (!kestrel::parse_rx_8852b(data + off, static_cast<size_t>(n) - off,
330-
f, drv_info_unit))
332+
f, drv_info_unit)) {
333+
devourer::emit_rx_parse_abort(_logger->events(), data + off,
334+
static_cast<size_t>(n) - off, off, n,
335+
f.payload_len, f.drvinfo_size,
336+
f.shift, parse_aborts);
331337
break;
338+
}
332339
if (f.rpkt_type == kestrel::RPKT_TYPE_PPDU && f.payload_len >= 8) {
333340
/* Full physts parse (header per-path rssi_td + IE01 avg SNR +
334341
* IE04..07 per-path SNR/EVM pages) — kestrel::parse_physts_8852.

‎tests/parse_abort_smoke.sh‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
#
3+
# parse_abort_smoke.sh — per-generation ambient-RX smoke for rx.parse_abort.
4+
#
5+
# The rx.parse_abort event (src/RxParseAbort.h) fires when a generation's RX
6+
# descriptor walk abandons a bulk-IN buffer on a malformed descriptor. Two
7+
# properties need hardware on every family: frames still flow (the walk is
8+
# untouched on the success path), and the all-zero-padding exclusion holds for
9+
# that family's aggregate format (no spurious event flood on ambient traffic).
10+
#
11+
# sudo bash tests/parse_abort_smoke.sh # every known plugged DUT
12+
# DUTS="0x8813 0xb812" sudo bash tests/parse_abort_smoke.sh
13+
set -u
14+
15+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
16+
BUILD=${BUILD:-$ROOT/build}
17+
CH=${CH:-6} # 2.4 GHz: ambient beacons guarantee RX traffic
18+
DWELL_S=${DWELL_S:-20}
19+
# J1 8814AU, J2 8822BU, J3 8812CU, Kestrel. The J3 default is the 8812CU, not
20+
# the 8822EU: the 8822E's DPDT front end decodes no ambient 2.4 GHz on this
21+
# bench (green init, DIG sees energy, zero frames) while its 5 GHz RX is
22+
# proven — the walk under test is identical on both dies.
23+
DUTS=${DUTS:-"0x8813 0xb812 0xc812 0x0101"}
24+
OUT=${OUT:-/tmp/parse-abort-smoke}
25+
26+
[ "$(id -u)" = 0 ] || { echo "must run as root"; exit 3; }
27+
[ -x "$BUILD/rxdemo" ] || { echo "build rxdemo first"; exit 3; }
28+
mkdir -p "$OUT"
29+
30+
MODS="rtw88_8812au rtw88_8821au rtw88_8822bu rtw88_8814au rtw88_8822cu rtw88_8822eu rtw89_8852bu rtw89_8852cu"
31+
BLACKLIST=/run/modprobe.d/zz-temp-blacklist-pabort.conf
32+
cleanup() {
33+
trap - EXIT INT TERM
34+
esc_build=$(printf '%s' "$BUILD" | sed 's/[][\\.^$*+?(){}|]/\\&/g')
35+
pkill -f "^$esc_build/rxdemo" 2>/dev/null
36+
rm -f "$BLACKLIST"
37+
wait 2>/dev/null
38+
}
39+
trap cleanup EXIT INT TERM
40+
mkdir -p "$(dirname "$BLACKLIST")"
41+
: > "$BLACKLIST"
42+
for m in $MODS; do echo "blacklist $m" >> "$BLACKLIST"; modprobe -r "$m" 2>/dev/null; done
43+
44+
rc=0
45+
for pid in $DUTS; do
46+
vid=0x0bda
47+
[ "$pid" = "0x0101" ] && vid=0x35bc
48+
log="$OUT/rx-${vid#0x}${pid#0x}.jsonl"
49+
echo "[pabort] DUT $vid:$pid — ${DWELL_S}s ambient RX on ch$CH"
50+
env DEVOURER_VID="$vid" DEVOURER_PID="$pid" DEVOURER_CHANNEL="$CH" \
51+
DEVOURER_RX_AGG_SA=any \
52+
DEVOURER_LOG_LEVEL=warn DEVOURER_EVENTS=stdout \
53+
timeout -s INT "$DWELL_S" "$BUILD/rxdemo" >"$log" 2>"$OUT/rx-${vid#0x}${pid#0x}.err"
54+
pkts=$(grep -cF '"ev":"rx.pkt"' "$log" || true)
55+
frames=$(grep -cF '"ev":"rx.frame"' "$log" || true)
56+
aborts=$(grep -cF '"ev":"rx.parse_abort"' "$log" || true)
57+
verdict=OK
58+
# rx.pkt samples (first 10 + every 100th) — >=2 proves the walk delivers.
59+
[ "${pkts:-0}" -ge 2 ] || { verdict="FAIL(no-rx)"; rc=1; }
60+
[ "${aborts:-0}" -eq 0 ] || { verdict="FAIL(aborts=$aborts)"; rc=1; }
61+
echo "[pabort] $vid:$pid rx.pkt=$pkts rx.frame=$frames parse_aborts=$aborts -> $verdict"
62+
done
63+
echo "[pabort] logs: $OUT"
64+
exit "$rc"

0 commit comments

Comments
 (0)