Skip to content

tx.report coverage attribution: fw emission ceiling, interleaved drops, dead MISSED_RPT_NUM (#363 step 1) - #368

Merged
josephnef merged 2 commits into
masterfrom
txrpt-coverage
Aug 4, 2026
Merged

tx.report coverage attribution: fw emission ceiling, interleaved drops, dead MISSED_RPT_NUM (#363 step 1)#368
josephnef merged 2 commits into
masterfrom
txrpt-coverage

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Step 1 of #363 (the issue stays open for SPE_RPT sampling and windowed RX receipts, steps 2–3).

What the recorded #359/#366/#367 ledgers + a 3-point envelope sweep establish

The 53% coverage collapse is a fw-side CCX emission ceiling, and the fw does not account its own drops.

demanded fps coverage achieved reports/s
1.04 k 100.0% 1.04 k (unsaturated)
1.26 k 99.4% 1.25 k — the knee
1.82 k 77.2% 1.40 k
2.39 k 53.4–55.6% 1.27–1.33 k

coverage ≈ min(1, ceiling/fps) with ceiling ≈ 1.3–1.4 k reports/s fits every measured point (8812CU TX).

Attribution to emission-time, not transport: 99.3% of unreported frames sit in SW_DEFINE tag gaps ≤ 2 (95 k singles + 47 k doubles at 2.39 k fps, max gap 28) — interleaved per-report loss, the signature of a rate limiter at report generation. Transport-batch loss (a dropped C2H aggregate) would lose contiguous dozens per event; the data shows the opposite. The tag-unwrap ground truth is trusted because the same runs' ledger joins matched 203,799/203,799 ok-frames in #359.

MISSED_RPT_NUM is dead on this fw: constant 4 on every report across all runs — including 100%-coverage runs where nothing was dropped. Parse verified bit-exact against the 8822B/8822C/8822E vendor headers (CCX_RPT_GET_MISSED_RPT_NUM: dword +0x4, bits [15:13] — identical in all three trees), so this is fw behaviour, not a devourer bug. Tag gaps are the only drop signal available.

What's in the PR

  • tests/txrpt_coverage_attrib.py — coverage, gap-shape histogram, missed-field distribution per recorded run (refuses tag-less J1 ledgers loudly).
  • src/TxReport.h — the missed_rpt field documents its measured deadness.
  • docs/scheduled-mac.md — the envelope + the consequence for accounting layers: keep the demanded report rate under ~1.3 k/s (SPE_RPT 1-in-N — step 2) or treat report-less frames as "unknown".

Validation

Three fresh on-air envelope runs (1.5 k/2 k/3 k fps demanded) + re-analysis of four recorded runs; tool parity-checked against the exploratory pass; ctest 48/48 (comment-only library change).

🤖 Generated with Claude Code

…nterleaved per-report drops, MISSED_RPT_NUM stuffed constant

tests/txrpt_coverage_attrib.py walks a recorded run's report stream and
prints coverage, the tag-gap run-length histogram, and the missed-field
distribution — the three facts that attribute a coverage collapse.

Measured (8812CU TX, arq_e2e ledgers, envelope sweep 1.0-2.4k fps): full
coverage to ~1.25k fps, then coverage ≈ ceiling/fps with the CCX emission
path saturating at ~1.3-1.4k reports/s (99.4% @ 1.26k, 77.2% @ 1.82k,
53-56% @ 2.39k). The excess drops per-report and interleaved — 99.3% of
unreported frames sit in tag gaps <= 2, the signature of an emission-time
rate limiter, not of transport-batch loss (a dropped C2H aggregate would
lose contiguous dozens). And the CCX MISSED_RPT_NUM field is stuffed with a
constant (4) on this fw, drops or no drops — parse verified bit-exact
against the 8822B/C/E vendor headers, so the fw offers no drop accounting
and SW_DEFINE tag gaps are the only drop signal. TxReport.h documents the
dead field; docs/scheduled-mac.md carries the envelope and the consequence:
above the ceiling, sample SPE_RPT 1-in-N or account report-less frames as
unknown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Attribute tx.report coverage loss to fw emission ceiling; add analysis tool + docs

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Add a tx.report coverage attribution analyzer for recorded arq_e2e runs.
• Document fw-side CCX report emission ceiling and interleaved drop signature.
• Clarify MISSED_RPT_NUM is constant on 8812CU; rely on SW_DEFINE tag gaps.
Diagram

graph TD
  A("Recorded arq_e2e run") --> B["drone.jsonl (tx.report/tx.stats)"] --> C["tests/txrpt_coverage_attrib.py"] --> D["Coverage + gap stats"] --> E["docs/scheduled-mac.md"]
  C --> F["src/TxReport.h"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fold attribution into tests/arq_e2e_analyze.py
  • ➕ Single canonical analysis entrypoint for ARQ runs
  • ➕ Can reuse existing tag-unwrapping and ledger-join machinery
  • ➕ Easier to keep output stable for regression tracking
  • ➖ Makes arq_e2e_analyze.py broader/less focused
  • ➖ Harder to run as a quick standalone check when only coverage is needed
2. Emit derived coverage/gap metrics as JSON for CI/regression dashboards
  • ➕ Machine-readable output enables trend tracking across firmware/hardware
  • ➕ Simplifies comparisons across runs without scraping stdout
  • ➖ Adds output format/versioning burden
  • ➖ Not necessary for the initial attribution step

Recommendation: Current approach (standalone attribution script + explicit docs + header-field note) is appropriate for step 1: it’s low-risk, easy to run on existing recorded runs, and clearly captures the attribution logic (gap-shape + achieved rpt/s + missed-field distribution). Consider folding it into arq_e2e_analyze.py or adding JSON output only if you want automated regressions/trending across devices/firmware.

Files changed (3) +111 / -1

Tests (1) +92 / -0
txrpt_coverage_attrib.pyAdd analyzer for coverage, tag-gap histogram, and missed-field distribution +92/-0

Add analyzer for coverage, tag-gap histogram, and missed-field distribution

• Introduces a Python tool that parses a recorded run’s drone.jsonl to compute report coverage (reports/submitted), achieved reports/s, run-length histogram of tag gaps (unreported frames), and the distribution of the missed field. Fails loudly when no tagged tx.report events exist (e.g., Jaguar1 format).

tests/txrpt_coverage_attrib.py

Documentation (2) +19 / -1
scheduled-mac.mdDocument CCX emission ceiling and accounting implications +12/-0

Document CCX emission ceiling and accounting implications

• Adds a measured coverage envelope showing a ~1.3–1.4k reports/s CCX emission ceiling on 8812CU TX. Documents that drops are interleaved (gap ≤2 dominated) and MISSED_RPT_NUM is constant, so SW_DEFINE tag gaps are the only reliable drop signal and higher rates must be sampled or treated as unknown.

docs/scheduled-mac.md

TxReport.hClarify MISSED_RPT_NUM deadness on 8812CU/Jaguar3 +7/-1

Clarify MISSED_RPT_NUM deadness on 8812CU/Jaguar3

• Updates the TxReport.missed_rpt field comment to reflect bench measurements: the firmware always reports a constant value (4) regardless of actual drops. Points readers to SW_DEFINE tag gaps and the scheduled-mac doc for measured ceiling and gap behavior.

src/TxReport.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. txrpt_coverage_attrib skips missed deltas ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
The ticket-derived checklist expects attribution to explicitly reconcile summed missed deltas
against unreported frames and to document a 1/2/3/4k fps sweep; the new analyzer only reports a
missed value distribution and the docs show only a few sweep points. This leaves PR Compliance ID
1’s Success Criteria not fully satisfied as written.
Code

tests/txrpt_coverage_attrib.py[R55-56]

+            missed_vals[int(ev.get("missed", 0))] += 1
+            t = ev.get("t")
Evidence
The checklist’s Success Criteria requires an analyzer comparison of missed deltas vs unreported
count and a documented 1/2/3/4k sweep (PR Compliance ID 1). The new analyzer only counts and prints
missed values (no delta/reconciliation), and the added documentation lists only the
1.26k/1.82k/2.39k points rather than the full requested sweep.

Document tx.report coverage-vs-rate curve and attribute report loss source
tests/txrpt_coverage_attrib.py[38-88]
docs/scheduled-mac.md[211-221]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PR Compliance ID 1 expects the attribution pass to compare summed `missed` deltas vs the unreported-frame count, and expects a documented 1/2/3/4k fps sweep.
## Issue Context
The current script counts `missed` values but never computes deltas or checks whether the `missed` field accounts for the observed unreported frames; the documentation currently lists only a subset of sweep points.
## Fix Focus Areas
- tests/txrpt_coverage_attrib.py[38-88]
- docs/scheduled-mac.md[211-221]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Missing tx.report timestamps ✓ Resolved 🐞 Bug ≡ Correctness
Description
tests/txrpt_coverage_attrib.py computes achieved rpt/s from the "t" field on tx.report events, but
tx.report does not emit "t" in the current schema/emitter, so the script can never produce a real
achieved rate. This silently degrades the tool’s output (rate prints as absent/0) and can mislead
the coverage-envelope interpretation.
Code

tests/txrpt_coverage_attrib.py[R56-59]

+            t = ev.get("t")
+            if t is not None:
+                t_first = t if t_first is None else t_first
+                t_last = t
Evidence
The new script reads "t" from tx.report to compute dur_s/rate, but the repo’s documented schema and
the tx.report emitter show that tx.report doesn’t include a timestamp field, and Event.h documents
that per-frame events omit it—so the achieved-rate path cannot work as written.

tests/txrpt_coverage_attrib.py[56-72]
docs/logging.md[106-113]
src/TxReport.h[106-123]
src/Event.h[208-210]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tests/txrpt_coverage_attrib.py` tries to compute an achieved report rate using `ev.get("t")` from `tx.report` events. However, `tx.report` is a per-frame event and currently does not include a `t` field in the documented schema, and the emitter (`emit_tx_report`) does not call `Ev::t()`.
### Issue Context
- `docs/logging.md` lists `tx.report` fields and does not include `t`.
- `src/TxReport.h::emit_tx_report()` emits `tx.report` without calling `.t()`.
- `src/Event.h` explicitly documents that per-frame events generally omit `t`.
### Fix Focus Areas
- tests/txrpt_coverage_attrib.py[56-72]
- src/TxReport.h[106-123]
- examples/tx/main.cpp[2195-2201]
- examples/tx/main.cpp[2275-2286]
- docs/logging.md[106-113]
### Suggested fix
Pick one (and make it explicit in output):
1) **Make a timestamp available for rate math**: add `.t()` when emitting `tx.stats` (and/or `tx.report` if acceptable), then have `txrpt_coverage_attrib.py` compute duration from those timestamped events.
2) **If timestamps are intentionally absent**, remove the achieved-rate computation or print a clear message like `achieved=unknown (no t field in logs)` rather than implying 0.
Also, if you keep the current logic, change the duration guard to use `t_last is not None` instead of `t_last` truthiness, so `t_last=0` doesn’t force `dur_s=0`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Import triggers execution ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
tests/txrpt_coverage_attrib.py runs analysis at import time via a top-level loop over sys.argv,
which can cause unexpected file access or SystemExit if the module is imported by other tooling.
This breaks normal Python module expectations and makes reuse brittle.
Code

tests/txrpt_coverage_attrib.py[R91-92]

+for rd in sys.argv[1:]:
+    analyze(rd)
Evidence
The new file ends with an unconditional loop over argv (runs on import). Comparable scripts in the
same tests/ directory use a __main__ guard, indicating the established repo convention and avoiding
import side effects.

tests/txrpt_coverage_attrib.py[91-92]
tests/arq_fec_dimension.py[118-119]
tests/ack_txreport_analyze.py[128-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The script executes immediately on import because it contains top-level code:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tests/txrpt_coverage_attrib.py
Comment thread tests/txrpt_coverage_attrib.py
Comment thread tests/txrpt_coverage_attrib.py Outdated
- tx.report events carry t (monotonic ms) — the CCX emission ceiling is a
  reports-per-second quantity and was previously unmeasurable from the
  stream itself (the envelope numbers were derived by hand from run spans).
  Verified on air: the attribution tool now computes achieved rpt/s live.
- txrpt_coverage_attrib.py prints the literal fw-acknowledged-drops vs
  tag-gap reconciliation whenever a fw actually populates MISSED_RPT_NUM
  (with the 3-bit saturation caveat), keeps the constant-field diagnosis
  shortcut, and gains the main() guard.
- docs/logging.md: tx.report row carries t + the dead-missed-field note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit f0c6e13 into master Aug 4, 2026
26 checks passed
@josephnef
josephnef deleted the txrpt-coverage branch August 4, 2026 02:39
josephnef added a commit that referenced this pull request Aug 4, 2026
…der the fw emission ceiling (#363 step 2) (#369)

Step 2 of #363 (step 3, windowed RX receipts, remains).

## What it does

The report knob becomes the sampling divisor N: SPE_RPT is requested on
every Nth frame (0 = off, 1 = every frame — previous semantics
unchanged), while the halmac SW_DEFINE tag still stamps **every** frame.
Received-tag deltas are therefore exact multiples of N; any other delta
is a dropped report (k·N = k−1 sampled reports lost, non-multiples are
anomalies). On the 8812 report format (no tag echo) sampling only
relieves the rate, via a new per-device counter.

Spec derived in #368: the CCX emission path saturates at ~1.3–1.4 k
reports/s, so pick **N ≥ fps/1300** and coverage of the sampled frames
is deterministic instead of load-collapsed.

## Measured (on air, 2.4 k fps)

| run | coverage | achieved rpt/s | note |
|---|---|---|---|
| N=1 control | 56.4% | 1,329 | the collapse persists — fifth
independent ceiling measurement |
| **N=2** | **100.0% of sampled** (191,298/191,323) | 1,107 | **zero
lost, zero off-modulo anomalies** |
| N=1 @ 1 k fps | 99.9%, no tag gaps | 749 | old behaviour
byte-for-byte; arq_e2e verdict stays acked_undelivered=0 |

Statistical-accounting check: the N=2 sampled ok-rate read **99.71%
against a 99.99% ledger ground truth** — pessimistic by the known
ACK-loss asymmetry (`ok=0`-but-delivered), the safe direction for a
delivery-accounting consumer.

## Changes

- `DeviceConfig::Tx::report` bool → int (doc-tagged with the ceiling +
the N ≥ fps/1300 rule); env parse clamps 0..255 (tag-unwrap ambiguity
bound).
- J2/J3: tag every frame, request every Nth (both fields inside the
checksummed span, re-checksum unchanged). J1: new `_tx_ccx_ctr` drives
the cadence.
- `tests/txrpt_coverage_attrib.py --sample-n` — expected-delta
accounting for sampled ledgers.
- `tests/arq_e2e_delivery.sh` — `DRONE_REPORT_N` passthrough.
- `docs/scheduled-mac.md` — the knob documented next to the measured
envelope, favourable and adversarial numbers together.

## Validation

Three fresh on-air runs (money/control/regression above); `ctest` 48/48;
existing matrix scripts unaffected (they pin `DEVOURER_TX_REPORT=1`,
whose semantics are unchanged).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
josephnef added a commit that referenced this pull request Aug 4, 2026
…he rx.seq ledger (#363 step 3) (#370)

Closes #363.

## The tier this completes

The ARQ campaign established the two lower tiers' limits: the hardware
ACK's horizon is chip-FIFO admission (it can never confirm host delivery
— #359), and per-frame CCX reports collapse against the fw's ~1.3 k
reports/s emission ceiling (#368, sampled in #369). This PR adds the
tier neither can reach: **the receiving application counts what it
consumed and mails it back.**

- `src/cell/RxReceipt.h` — `ReceiptWindow` (receiver: sliding ring
bitmap over the last W frame indices) + `ReceiptLedger` (transmitter:
idempotent merge of overlapping, versioned receipt TLVs; strict-prefix
parse; absorbs only receipts naming its own TA). Same caller-side
contract as `UeRxAttribution`: demos feed frames, RX loops untouched, no
payload convention assumed.
- duplex: notes on the existing `rx.seq` path, injects a receipt frame
every `DEVOURER_RX_RECEIPT_MS` on the feedback path (802.11 data at 6M;
concurrent `send_packet` callers serialize on a demo mutex).
`DEVOURER_RX_RECEIPT_WINDOW` sizes coverage (default 8192).
- txdemo: `DEVOURER_TX_RECEIPTS` absorbs and emits **every** receipt as
`tx.receipt` with the raw TLV hex.
- `tests/receipt_verify.py`: replays the TLV merge and demands
**set-equality** with the receiver's own `rx.seq` ledger — the issue's
acceptance bar, literally.
- `receipt_roundtrip` ctest selftest; `RECEIPT_MS` knob in the arq
harness; docs (logging.md rows, scheduled-mac.md tier paragraph).

## Measured (on air)

| regime | frames verified | receipts | verdict |
|---|---|---|---|
| clean, 5 cycles | 126,594 | 1,671 | **FRAME-EXACT** |
| 150 ms consumer stalls @ 2.4 k fps, spsc-fat parking ring | 349,455 |
1,330 | **FRAME-EXACT** |

## Two bench-taught lessons, in the code

1. **Received bodies carry the trailing FCS** — the first live run
absorbed zero receipts because the TLV parse demanded exact length; an
ACK frame logging `len:14` (10 + FCS) was the tell. The parse is
strict-prefix now, with the TA-match keeping arbitrary payloads out.
2. **The window must exceed the worst backlog drain in frames** — with a
2,048-bit window, a stalled spsc-fat pool draining ~3 k frames inside
one receipt interval evicted 2,846 delivered frames before any receipt
covered them (`MISSING` in the verifier, `late=0` — eviction, not
reordering). The sizing rule is at the class comment: `window_bits >
pool_bytes / min_frame_bytes + one encode interval of arrivals`; the
8192 default clears this bench's worst case ~2.7×.

## Validation

`ctest` 49/49 (new selftest included); four on-air runs (first-light
smoke, full-scale clean, failed 2048-window stress — kept as the sizing
lesson — and the passing 8192-window stress). The verifier's
`NO-RECEIPTS` and `MISMATCH` paths were both exercised for real during
bring-up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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