Skip to content

DEVOURER_TX_REPORT=N: sampled CCX reports — deterministic coverage under the fw emission ceiling (#363 step 2) - #369

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

DEVOURER_TX_REPORT=N: sampled CCX reports — deterministic coverage under the fw emission ceiling (#363 step 2)#369
josephnef merged 2 commits into
masterfrom
txrpt-sampling

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

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.shDRONE_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

…erage under the fw emission ceiling

The CCX emission path saturates at ~1.3-1.4k reports/s, so per-frame SPE_RPT
above ~1.25k fps collapses coverage load-dependently (53-56% at 2.4k fps).
The report knob is now the sampling divisor N: request a report on every Nth
frame (0 = off, 1 = every frame — the previous semantics unchanged), while
the halmac SW_DEFINE tag still stamps EVERY frame, so received-tag deltas
are exact multiples of N and any other delta is a dropped report. On the
8812 report format (no tag echo) sampling only relieves the rate; a new
per-device counter drives the cadence there.

Measured (2.4k fps, on air): N=1 control collapses to 56.4% (achieved 1,329
rpt/s — the ceiling again); N=2 delivers 100.0% of the sampled reports
(191,298/191,323, zero lost, zero off-modulo anomalies, achieved 1,107
rpt/s) and the sampled ok-rate reads 99.71% against a 99.99% ledger truth —
pessimistic by the ACK-loss asymmetry, the safe direction. N=1 at 1k fps is
byte-for-byte the old behaviour: 99.9% coverage, no tag gaps, and the
arq_e2e verdict stays acked_undelivered=0.

txrpt_coverage_attrib.py gains --sample-n (expected-delta accounting:
k*N = k-1 sampled reports lost, non-multiples flagged as anomalies);
arq_e2e_delivery.sh gains the DRONE_REPORT_N passthrough;
docs/scheduled-mac.md documents the knob next to the measured envelope.

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

Copy link
Copy Markdown

PR Summary by Qodo

DEVOURER_TX_REPORT as sampling divisor for deterministic CCX report coverage

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Change DEVOURER_TX_REPORT from boolean to sampling divisor N (0..255) to stay under CCX emission
 ceiling.
• Keep per-frame SW_DEFINE tagging on HalMAC chips; request reports every Nth frame for
 deterministic sampled coverage.
• Update coverage attribution tooling, e2e bench wiring, and docs with measured ceiling guidance.
Diagram

graph TD
  A["DEVOURER_TX_REPORT=N"] --> B["env_config.cpp"] --> C["DeviceConfig.tx.report (N)"] --> D["RtlJaguar* build_tx_block"] --> E{{"FW CCX report emit"}} --> F[("tx.report events")] --> G["txrpt_coverage_attrib.py"] --> H["scheduled-mac.md"]

  subgraph Legend
    direction LR
    _cfg["Config knob"] ~~~ _code["Code path"] ~~~ _fw{{"Firmware"}} ~~~ _data[("Event stream")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Auto-compute N from requested FPS (gap_us)
  • ➕ Removes manual tuning; callers just set a target rate and get safe sampling.
  • ➕ Avoids misconfiguration where N is too small and coverage collapses.
  • ➖ FPS is not always known/constant (bursty traffic, external pacing), so auto-derived N can be wrong.
  • ➖ Less explicit/reproducible for benches; changes behavior implicitly across workloads.
2. Adaptive sampling based on observed tag gaps / missed-rate
  • ➕ Can react to real device/firmware ceilings and host load dynamically.
  • ➕ Potentially maximizes coverage while staying under the emission ceiling.
  • ➖ Control-loop complexity; risk of oscillation and harder-to-debug nondeterminism.
  • ➖ Requires stable observation window and may lag during fast rate changes.

Recommendation: Keep the explicit divisor-N knob as implemented: it preserves deterministic, reviewable behavior and makes the sampled coverage invariant (HalMAC tag deltas are exact multiples of N) easy to validate. If needed later, add optional helper logic in bench scripts (not the device core) to suggest an N from gap_us/fps, rather than making runtime behavior implicit.

Files changed (9) +103 / -36

Enhancement (5) +48 / -18
DeviceConfig.hChange tx.report from bool to int with sampling semantics documentation +11/-2

Change tx.report from bool to int with sampling semantics documentation

• Updates DeviceConfig::Tx::report from bool to int defaulting to 0. Expands the docstring to define sampling divisor behavior, the CCX emission ceiling motivation, and the tag-delta invariant expected by consumers/tests.

src/DeviceConfig.h

RtlJaguarDevice.cppSample SPE_RPT on Jaguar1 using per-device counter +10/-4

Sample SPE_RPT on Jaguar1 using per-device counter

• Updates DEVOURER_TX_REPORT handling so SPE_RPT is set only on every Nth frame. Uses a new atomic counter to drive the cadence since the 8812 report format lacks SW_DEFINE tag echo, so sampling only reduces report rate (not attribution granularity).

src/jaguar1/RtlJaguarDevice.cpp

RtlJaguarDevice.hAdd atomic CCX sampling counter for Jaguar1 report cadence +5/-0

Add atomic CCX sampling counter for Jaguar1 report cadence

• Introduces _tx_ccx_ctr (atomic uint32) used to request CCX reports on every Nth frame when cfg.tx.report > 0. Comment clarifies Jaguar1’s lack of tag echo and the purpose of the counter.

src/jaguar1/RtlJaguarDevice.h

RtlJaguar2Device.cppTag every frame; sample SPE_RPT every Nth on Jaguar2 +11/-6

Tag every frame; sample SPE_RPT every Nth on Jaguar2

• Changes report request logic to always advance the SW_DEFINE tag per frame while only setting SPE_RPT when the per-frame tag counter modulo N hits zero. Keeps checksum recalculation behavior unchanged (idempotent).

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar3Device.cppTag every frame; sample SPE_RPT every Nth on Jaguar3 +11/-6

Tag every frame; sample SPE_RPT every Nth on Jaguar3

• Mirrors the Jaguar2 approach: SW_DEFINE tag increments every frame for continuous correlation, while SPE_RPT is asserted only every Nth frame. Preserves descriptor checksum recalculation and documents the emission-ceiling rationale and drop detection via off-modulo deltas.

src/jaguar3/RtlJaguar3Device.cpp

Tests (2) +45 / -15
arq_e2e_delivery.shPlumb DRONE_REPORT_N through to DEVOURER_TX_REPORT +2/-1

Plumb DRONE_REPORT_N through to DEVOURER_TX_REPORT

• Adds DRONE_REPORT_N (default 1) to control CCX report sampling for the drone-sim in the e2e delivery bench. Passes the value through to DEVOURER_TX_REPORT so benches can stay under the firmware emission ceiling at higher fps.

tests/arq_e2e_delivery.sh

txrpt_coverage_attrib.pyAdd --sample-n and sampled-coverage accounting +43/-14

Add --sample-n and sampled-coverage accounting

• Extends the analyzer to accept sample_n and compute expected sampled reports (submitted//N). Interprets tag deltas as multiples of N (k*N implies k-1 sampled reports lost) and flags non-multiples as anomalies; retains legacy coverage and gap histogram output for sample_n=1.

tests/txrpt_coverage_attrib.py

Documentation (1) +8 / -2
scheduled-mac.mdDocument DEVOURER_TX_REPORT sampling and N>=fps/1300 rule +8/-2

Document DEVOURER_TX_REPORT sampling and N>=fps/1300 rule

• Replaces the previous generic sampling guidance with the new semantics: DEVOURER_TX_REPORT=N requests a report every Nth frame while tagging every frame. Adds measured results (2.4k fps control vs N=2) and the rule of thumb to pick N to stay under the ~1.3–1.4k reports/s firmware ceiling.

docs/scheduled-mac.md

Other (1) +2 / -1
env_config.cppParse DEVOURER_TX_REPORT as integer sampling divisor (clamped) +2/-1

Parse DEVOURER_TX_REPORT as integer sampling divisor (clamped)

• Switches DEVOURER_TX_REPORT parsing from a boolean flag to env_long(). Clamps the parsed value to 0..255 and stores it into cfg.tx.report.

examples/common/env_config.cpp

@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


Action required

1. Sampling wrap breaks cadence ✓ Resolved 🐞 Bug ≡ Correctness
Description
Jaguar2/Jaguar3 gate SPE_RPT with _tx_rpt_tag (16-bit) modulo N, so the sampling phase resets
every 65,536 frames and produces off-modulo tag deltas even when no reports were dropped. This
violates the PR’s deterministic-sampling contract for long-running sessions (e.g., N=3 yields a
delta of 1 at wrap).
Code

src/jaguar2/RtlJaguar2Device.cpp[R1531-1534]

+    const uint16_t k = _tx_rpt_tag.fetch_add(1);
+    SET_TX_DESC_SPE_RPT_8822B(
+        out, k % static_cast<uint16_t>(_cfg.tx.report) == 0 ? 1 : 0);
+    SET_TX_DESC_SW_DEFINE_8822B(out, k & 0xff);
Evidence
The PR changes Jaguar2/Jaguar3 to sample reports using k % N == 0, but k comes from a uint16_t
counter. A uint16_t wrap causes the sampling condition to restart at 0, producing a short (or
otherwise non-N) interval across wrap for most N; the analyzer computes tag deltas modulo 256, so
this shows up as an off-modulo delta even with zero drops.

src/jaguar2/RtlJaguar2Device.cpp[1523-1535]
src/jaguar2/RtlJaguar2Device.h[251-253]
src/jaguar3/RtlJaguar3Device.cpp[2001-2013]
src/jaguar3/RtlJaguar3Device.h[251-253]
tests/txrpt_coverage_attrib.py[60-64]

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

## Issue description
Jaguar2/Jaguar3 decide whether to request a CCX report via `k % N == 0`, but `k` is taken from `_tx_rpt_tag`, which is only 16 bits. When it wraps (every 65,536 frames), the next sampled index is `0`, and the interval between requested reports across the wrap becomes `65536 % N` (not `N`), producing non-multiple-of-N tag deltas and false “dropped/anomaly” signals.
### Issue Context
- The low 8 bits of SW_DEFINE are echoed back in the report; the analyzer checks `(tag - prev_tag) % 256`.
- Sampling correctness depends on the request cadence being stable across long runs.
### Fix Focus Areas
- src/jaguar2/RtlJaguar2Device.h[251-253]
- src/jaguar3/RtlJaguar3Device.h[251-253]
- src/jaguar2/RtlJaguar2Device.cpp[1531-1534]
- src/jaguar3/RtlJaguar3Device.cpp[2009-2012]
### Implementation notes
- Change the counter used for sampling to `std::atomic<uint32_t>` (either widen `_tx_rpt_tag` or add a separate `_tx_rpt_ctr`).
- Keep SW_DEFINE as `k & 0xff` (still 8-bit), but compute `SPE_RPT` from the wide `k` so wrap is practically unreachable.

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



Remediation recommended

2. expected miscounts sampled reports ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
The sampled-ledger analyzer in tests/txrpt_coverage_attrib.py computes `expected = submitted //
sample_n`, which undercounts expected reports because sampling requests a report on frame 0 and then
every Nth frame (requiring ceiling division). This can inflate sampled coverage (even >100%), hide
dropped sampled reports, and distort loss attribution, violating the requirement that tooling
correctly interprets sampled ledgers.
Code

tests/txrpt_coverage_attrib.py[76]

+        expected = submitted // sample_n
Evidence
PR Compliance ID 2 requires analysis tooling to correctly interpret sampled ledgers, but the
analyzer’s sampled-path sets expected = submitted // sample_n (floor division). On the
device/firmware side, reports are requested when k % N == 0 with k starting at 0 (because
fetch_add(1) returns the pre-increment value), producing requests at indices 0, N, 2N, ...; this
implies there is at least one request for any non-empty run and, in general, the expected count
follows ceil(submitted / N) rather than floor division. Therefore the current computation can
undercount expected, making calculated sampled coverage erroneously high and masking losses.

Implement deterministic SPE_RPT sampling via DEVOURER_TX_REPORT=N and update analyzer for sampled ledgers
tests/txrpt_coverage_attrib.py[72-87]
tests/txrpt_coverage_attrib.py[72-89]
src/jaguar2/RtlJaguar2Device.cpp[1531-1534]

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` miscomputes the expected number of sampled reports for `--sample-n > 1` by using floor division (`expected = submitted // sample_n`). Because the device requests a report on index/frame 0 and then every Nth frame (`0, N, 2N, ...`), the correct expected count is `ceil(submitted / N)` (with `submitted == 0` yielding `expected == 0`), otherwise sampled coverage can be overstated (even >100%) and losses can be masked.
## Issue Context
This script is used to validate deterministic sampling behavior (`DEVOURER_TX_REPORT=N`) and to attribute/report coverage and loss. Device-side sampling uses the condition `k % N == 0` with `k` starting at 0 (due to `fetch_add(1)` returning the pre-increment value), so any non-empty submission run produces at least one expected sampled report even when `submitted < N`.
## Fix Focus Areas
- tests/txrpt_coverage_attrib.py[72-89]
- src/jaguar2/RtlJaguar2Device.cpp[1531-1534]

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


3. tx.report docs out-of-date ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
docs/aggregation.md still says DEVOURER_TX_REPORT sets SPE_RPT in every descriptor, but the PR
changes semantics to a sampling divisor N where reports are only requested every Nth frame. This can
mislead users configuring report sampling and interpreting coverage.
Code

src/DeviceConfig.h[R224-227]

+     * Value = sampling divisor N: 1 requests a report on EVERY frame, N > 1
+     * on every Nth (0..255). The CCX emission path saturates at ~1.3–1.4 k
+     * reports/s (docs/scheduled-mac.md), so above ~1.25 k fps pick
+     * N >= fps/1300 and coverage of the SAMPLED frames stays deterministic
Evidence
The docs explicitly claim SPE_RPT is set on every descriptor when the knob is enabled, but the
updated config comment and the Jaguar2/Jaguar3 descriptor build code implement divisor-based
sampling instead.

docs/aggregation.md[54-66]
src/DeviceConfig.h[215-232]
src/jaguar2/RtlJaguar2Device.cpp[1523-1534]

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

## Issue description
Documentation in `docs/aggregation.md` still describes `DEVOURER_TX_REPORT` / `tx.report` as a per-frame boolean that sets `SPE_RPT` on every descriptor. After this PR, `tx.report` is an integer sampling divisor N and `SPE_RPT` is only asserted every Nth frame.
### Issue Context
`src/DeviceConfig.h` now documents `tx.report` as “Value = sampling divisor N”. Jaguar2/Jaguar3 implement this by setting `SPE_RPT` based on `k % N == 0`.
### Fix Focus Areas
- docs/aggregation.md[54-66]
- src/DeviceConfig.h[215-232]
### Implementation notes
- Update the `docs/aggregation.md` section to describe: 0=off, 1=every frame, N>1=every Nth frame.
- Mention that HalMAC chips still stamp SW_DEFINE every frame (continuous tag) while SPE_RPT is sampled.

ⓘ 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 Outdated
Comment thread src/jaguar2/RtlJaguar2Device.cpp Outdated
Comment thread src/DeviceConfig.h
…ation, aggregation.md caught up

- The 16-bit tag counter's wrap at 65,536 jumps the sampling phase for any N
  that doesn't divide it — off-modulo tag deltas masquerading as dropped
  reports at every seam; J1's 32-bit request counter hits the same seam in
  ~20 days at field rates. Both are 64-bit now (seam unreachable); the tag
  byte and checksummed-span handling are unchanged.
- The sampled expectation ceils (the request fires on k%N==0, frame 0
  first) — odd totals no longer undercount by one.
- docs/aggregation.md's tx.report paragraph carries the divisor semantics
  and the ceiling instead of "every descriptor".

Validated: 48/48 ctest; on-air N=2 smoke under the widened counters —
sampled-coverage 100.0% (82,465/82,490 with the ceil expectation), zero
lost, zero off-modulo anomalies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit 9edbc6e into master Aug 4, 2026
26 checks passed
@josephnef
josephnef deleted the txrpt-sampling branch August 4, 2026 03:12
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