Skip to content

Kernel channel-switch baseline: measured matrix + go/no-go for standard-driver FHSS (exp 1/5)#313

Merged
josephnef merged 7 commits into
masterfrom
kernel-chansw-baseline
Jul 20, 2026
Merged

Kernel channel-switch baseline: measured matrix + go/no-go for standard-driver FHSS (exp 1/5)#313
josephnef merged 7 commits into
masterfrom
kernel-chansw-baseline

Conversation

@josephnef

@josephnef josephnef commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #269.

Measurement-only experiment: a like-for-like latency/behavior matrix of every channel-switch primitive the standard Linux/Realtek kernel drivers expose on devourer-supported silicon, with kernel-side instrumentation (ftrace trace_clock=mono, cfg80211/mac80211 events, grouped kprobes) and a mandatory on-air oracle — two devourer rxdemo receivers whose MAC-latched tsfl timestamps are mapped to host time by a robust linear fit (σ 40–280 µs), so dead air is measured at the RF, not at the API.

Headline results (full note: docs/kernel-channel-switch-baseline.md)

driver / primitive (RTL8822BU unless noted) on-air dead-air med p99 gate
vendor rtl88x2bu set-channel 36↔40 1.08 ms 4.42 ms advance
rtw88 set-channel 1↔6 (2.4 GHz) 6.1 ms 16.6 ms slow-slot-only
rtw88 set-channel 36↔40 52.0 ms 60.6 ms control
rtw89 set-channel 36↔40 (RTL8852BU) 31.5 ms 36.3 ms control
rtw89 AP CSA (count=1) 102.9 ms (≈1 TBTT) 205.8 ms control
rtw88 AP CSA policy-rejected (no AP_CSA flag)

The silicon retunes in ~1 ms; rtw88's 52 ms is its software sequence. Host-command latency alone (the number an API benchmark would report) overstates the 2.4 GHz RF transition by 14× — the oracle is not optional.

Go: exp-2 (#270) targets rtw_hal_ch_sw_oper_offload → H2C 0x1C CHNL_SWITCH_OPER_OFFLOAD (compiled + runtime-armed in the exact module that measured 1.08 ms); exp-3 (#271) targets the MCC H2C 0x10/0x11/0x18/0x19 family (verified live through mcc_enable=1).

What's in the PR

  • tests/kchansw_{bench,trace,inject,analyze}.py + kchansw_setup.sh — the reusable harness (tracefs session mgr with grouped kprobes + clean teardown, 2 kHz tagged injector immune to kernel seq rewrite, offline analyzer with --self-test, wedge-recovery ladder up to a PCI controller revive).
  • tests/kchansw_vm.sh + kchansw_vm_dut.py — the vendor phase runs in the pinned-kernel VM: the vendor TDLS-variant build livelocks the kernel in its cfg80211 add-interface op with RTNL held (took the host down twice before isolation; reproducible on demand). DUT is USB-passed-through, oracles stay host-side, VM clock bridged by a min-RTT-midpoint probe (±0.15 ms); the host insmod path is gated behind KCHANSW_HOST_VENDOR_OK=1.
  • tests/kchansw_vendor_build.sh — TDLS/MCC variant builds of the reference/rtl88x2bu fork (mutually exclusive families); submodule gains two kernel-compat commits (tdls_mgmt link_id ≥ 6.0, timer_container_of ≥ 6.16).
  • tests/kchansw_b210_gap.py — B210 partial-band energy probe quantifying the decode-vs-energy oracle bias.
  • examples/rx/main.cpprx.frame per-frame events follow DEVOURER_RX_AGG_SA and carry an sa field (default behavior unchanged); the oracle enabler.
  • docs/kernel-channel-switch-baseline.md — method, capability matrix, distributions, endurance/USB-link findings, go/no-go.

Raw per-switch CSVs stay in /tmp (regenerable); the doc carries the summary tables per the agreed scope.

🤖 Generated with Claude Code

josephnef and others added 7 commits July 19, 2026 22:16
The per-frame rx.frame stream was hard-gated to the canonical TX SA, so an
RX oracle could not observe a foreign transmitter (a kernel-driver DUT's own
MAC address). When DEVOURER_RX_AGG_SA is set, its filter now also selects
the stream ("canon"/mac/"any"); unset keeps the historic canonical-only
stream byte-for-byte. rx.frame gains the matched SA as a hex field so an
"any"-gated consumer can attribute frames per transmitter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measures the channel-switch primitives a standard Linux/Realtek kernel
driver exposes — monitor set-channel, AP/CSA (wpa_supplicant AP mode),
remain-on-channel, single-freq active scan, TDLS capability, vendor MCC
attempt — with ftrace instrumentation (cfg80211/mac80211 events + kchansw/
kprobes on driver internals, trace_clock=mono) and a mandatory on-air
oracle: two devourer rxdemo receivers pinned to the source/destination
channels plus a 2 kHz tagged-frame injector whose payload trailer
(magic+nonce+counter+send-mono-ns) survives kernel seq rewrites. A switch
whose API succeeded without destination-channel RF evidence is a failed
measurement.

kchansw_bench.py   orchestrator (inventory/smoke/phase-a/cold/phase-b/-c);
                   reuses regress.py's adapter bind/VBUS/process machinery
kchansw_trace.py   tracefs session ctx-mgr, kprobe group, markers, cleanup
kchansw_inject.py  AF_PACKET paced injector with per-frame send log
kchansw_analyze.py offline: tsfl→mono robust fit, per-switch CSV,
                   median/p90/p99/max, <5/5-20/>20 ms gate verdicts;
                   --self-test fabricates a run with known truth
kchansw_vendor_build.sh  build reference/rtl88x2bu with CONFIG_TDLS=y
                   CONFIG_MCC_MODE=y (H2C 0x1C ch-sw offload + MCC family),
                   insmod rtw_wifi_spec=1, restore in-tree binding
kchansw_setup.sh   venv + preflight + launch wrapper

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aced

Session-0 findings folded back into the harness:

- tracefs kprobe_events rejects python text-mode append (its open() lseeks
  to EOF; tracefs → EINVAL): write through a raw O_APPEND fd. Disarm the
  kchansw/ event group before probe removal (armed kretprobes → EBUSY).
- rtw88_8822bu wedges under sustained monitor injection: once as a full
  USB disconnect (ENODEV, dmesg shows EPROTO then disconnect), once as
  silent TX (sends ack fine, nothing airs, dmesg clean). Detect both —
  ENODEV from iw, and an oracle frame-progress stall — and recover through
  a ladder (re-probe → module reload → authorized/VBUS cycle), respawning
  the injector; every later row is flagged post_recovery, so the wedge
  count itself becomes the endurance datum.
- oracle liveness gate now counts rx.frame decodes, not stdout lines.
- default oracle A is the 8821AU: the 8812AU in its bench position decodes
  <10% of near-field injector frames (60% CRC-broken at RSSI ~68) while
  the 8821AU/8812CU decode 90-97% clean.
- vendor build: TDLS and MCC are mutually exclusive (drv_conf.h #errors;
  MCC additionally needs CONCURRENT_MODE=y + BT_COEXIST=n) — VARIANT=tdls
  and VARIANT=mcc build modes replace the combined attempt.
- headline phase-a config runs 2× switches (≥1000 per direction).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the ko path, lsmod checks and module name; the earlier missing-symbol
warning was the wrong filename — rtw_hal_ch_sw_oper_offload is present in
the TDLS-variant build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headline run escalated past every soft recovery: the T3U wedged its
uPD720201 host controller outright (re-probe -110), and only a PCI bus
reset + D3hot→D0 bounce + rescan followed by a 12 s per-port power-off
brought it back — re-enumerated on the USB2 companion bus. Encode that as
the ladder's final rung (with VID:PID rediscovery, since recovery moves
the device across buses), verify DUT aliveness between configs, add
ROC-specific distributions (grant/dwell/return) to the analyzer, and add
tests/kchansw_b210_gap.py — a partial-band B210 energy-gap probe that
compares energy-silence gaps against decode-derived gaps statistically,
needing no cross-clock alignment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he host

Kprobing was innocent: the vendor TDLS-variant build (no CONCURRENT_MODE)
livelocks the kernel in its cfg80211 add-interface op with RTNL held (ssh
dead, dmesg blocked, no panic — power cycle to recover), isolated
step-by-step after it took the host down twice via the ROC config's
injector monitor vif. phase-b-vm runs the DUT + trace session inside the
pinned-kernel VM (kchansw_vm.sh prepare/mcc/restore + kchansw_vm_dut.py),
oracles stay on the host, and a min-RTT-midpoint ssh probe maps the VM
clock onto the host timeline (analyzer applies meta vm_clock_shift_ns;
VM-internal spans are shift-invariant). The host phase-b path is gated
behind KCHANSW_HOST_VENDOR_OK=1. ROC on the vendor build is trace-only
(no second vif possible); the MCC attempt uses the auto-created
CONCURRENT_MODE vif and the mcc/ proc directory, persisting each step so
a mid-step wedge loses nothing. udev's wlan0->wlxMAC rename race gets a
settle+re-resolve in dut_up and the helper bring-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full like-for-like matrix for issue 269: rtw88_8822bu (set-channel /
ROC / scan / CSA-policy-rejected / cold / endurance incl. the USB3 wedge
ladder), the vendor rtl88x2bu in the VM (set-channel 36<->40 dead-air
median 1.08 ms p99 4.42 ms -> ADVANCE, 48x under rtw88 on the same
silicon; trace-only ROC; MCC machinery live to mcc_enable=1), and
rtw89_8852bu (set-channel 31 ms; CSA works — TBTT-quantized ~1-2 beacon
intervals; hardware ROC 53 ms grant). Go: exp-2 targets
rtw_hal_ch_sw_oper_offload -> H2C 0x1C, exp-3 the MCC H2C
0x10/0x11/0x18/0x19 family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit ca6bfda into master Jul 20, 2026
18 checks passed
@josephnef
josephnef deleted the kernel-chansw-baseline branch July 20, 2026 08:08
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.

Standard-driver FHSS experiment 1/5: benchmark channel-switch primitives

1 participant