Telemetry page: both directions, every backend, and the counters behind them - #91
Merged
Conversation
…nd them ## Summary The diagnostics page showed inbound UDP only, named just the preferred backend (so a Windows box with both drivers only ever said "ViGEm"), and painted an idle-but-healthy host red. It also kept polling `/api/debug` twice a second forever once visited. `GET /api/debug` gains `rx`, `tx`, `audio` and `auth` blocks plus the host gauges the page had been inferring, and the page is rebuilt around them. ## Server - `src/app/wire_stats.h`: one header-only `WireStats` for per-message wire counters, so the boundary is visible in one place. Header-only inline, so no per-platform `globals.cpp` can drift. - Inbound: per-type counts in the receiver's cold dispatch branch, and the rejections that never reach a decoder (runt, unknown token, malformed, unknown type). `DispatchResult` gained a `handled` flag so the receiver can split "known opcode, failed its length guard" from "unrecognised" without re-parsing, which keeps `inner_dispatch.cpp` globals-free as its portable test build requires. - Outbound: counted centrally in `ClientAdapter::sendEncryptedPacket`, which now also checks `sendto`'s return value (it was discarded). - Audio: `SessionService` counts mic frames accepted/late/dropped and decoded/FEC-recovered/concealed, and speaker frames sent, suppressed as digital silence, encode-failed and lost to lock contention. Core-pure atomics, no new locks. - `peakLoopUs`: `maxLoopUs` keeps its read-and-zero window semantics for the bench harness, but the receiver's thread-local high-water mark means a zeroed value only rises again on a new all-time record, so the page's "peak" read 0 nearly always. `/api/debug` now folds each windowed read into a true peak. - Also fixed: `webPort` was read by the page but never sent. ## Hot path Untouched. Nothing was added to the accepted-packet path: the inbound counters sit only in rejection branches and the cold non-gamepad branch, no outbound message is reachable from the gamepad path, and the audio counters run at 50 Hz per controller under locks those paths already hold. Inbound byte counting is deliberately absent because it would cost an atomic add per packet. ## Page Bidirectional flow diagram, six grouped sections (54 rows), a mirrored in/out traffic chart, and a list of every backend the host reports with vendor, mode, audio capability, lifecycle and installed-vs-bundled driver version. Idle now reads idle rather than error, polling stops on navigation, and an older satellite degrades to em dashes instead of `undefined`. 80 debug strings across all six locales. ## Tests - New `test_wire_stats` (108 assertions): every opcode's mapping in both directions, the malformed/unknown split, reset coverage checked generically so a forgotten counter fails, and the peak fold. - `test_receiver`: `handled` asserted for every accepted type and every length-guard rejection, including that a well-formed frame the service then refuses on policy still counts as handled. - `test_session_service`: audio counters cross-checked against the existing concealment/reorder scenario and the silence-suppression one. - `test_status_json`: golden shape updated; new test pins the counter blocks to `/api/debug` and keeps them off the hot status/SSE surfaces. - Full ctest 35/35, clang-format clean, core purity gate clean. - Page verified by rendering it headless against five payload shapes (busy, idle, driver-missing, pre-2.0.0 server, Linux/uinput) and two extra locales, and against the live 1.1.0-dev server for backwards compatibility. ## Docs `docs/contract.md` documents the new `/api/debug` fields under the admin API; `docs/architecture.md` records the counter placement rule and the peak-vs-window distinction; CHANGELOG 2.0.0 gains a paragraph.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The Debug Telemetry page was showing one direction, one backend, and one
misleading colour.
GET /api/debugwas eight inbound counters plus thepreferred backend's name, so on a Windows box with both drivers the page
only ever said "ViGEm"; it painted an idle-but-healthy host red because it
read
backendAvailable(which means "the bus is open right now", not "thedriver works"); and it kept polling
/api/debugtwice a second for the lifeof the tab once you had visited it.
GET /api/debugnow carriesrx,tx,audioandauthblocks plus thehost gauges the page had been guessing at, and the page is rebuilt around
them.
Server
rxmalformed= known opcode that failed its length guard,unknownType,runt,unknownToken)txunroutable/encryptFailed/oversize/sendFailedaudioauthnotPaired/badProofPlus
peakLoopUs,webPort,mdnsResponderActive,clientApiListening,live connection and controller counts, and
sessionsReaped.Two bugs fixed on the way:
sendto's return value was discarded inClientAdapter, so anENOBUFSorEHOSTUNREACHwas invisible. It is checked now and feedstx.sendFailed.maxLoopUsreads 0 nearly always./api/debugzeroes it on read,but the receiver's thread-local high-water mark never resets, so the
atomic only climbs again on a new all-time record. The window semantics
are kept (the local bench harness reads them), and a new
peakLoopUsfolds each windowed read into the true peak.
webPortwas read by the page but never sent by the server.Hot path
Untouched, and the split is deliberate rather than incidental. Nothing was
added to the accepted-packet path:
cold non-gamepad dispatch branch;
DispatchResultgained ahandledflag so the receiver can tell amalformed frame from an unrecognised one without re-parsing, which is
what keeps
inner_dispatch.cppfree of globals (its test target does notlink
globals.cpp);sendEncryptedPacketis free;already hold.
Inbound byte counting is deliberately absent: it would cost an atomic add
per packet on the accepted path.
src/app/wire_stats.his header-only with aninlineglobal, so there isno per-platform
globals.cppfor a new counter to drift out of.Page
Bidirectional flow diagram, six grouped sections (54 rows), a mirrored
in/out traffic chart, and a list of every backend the host reports with
its vendor, kernel/user mode, audio capability, lifecycle and
installed-versus-bundled driver version. Idle reads idle instead of error,
polling stops on navigation, and a pre-2.0.0 satellite degrades to em
dashes rather than rendering
undefined. 80 debug strings across all sixlocales.
Type of Change
How Has This Been Tested?
ctest/test_session_service/test_windows_platform)Automated. Full
ctest35/35 on the MinGW lane,scripts/check-format.shclean against clang-format 22.1.4,
scripts/check_core_purity.shclean.test_wire_stats(108 assertions): every opcode's mapping in bothdirections, the malformed-vs-unknown split, the deleted registration
opcodes staying "unknown", the peak fold across a zeroing read, and
reset()coverage checked generically throughforEachCounter, soforgetting to reset a future counter fails the test rather than silently
leaking across a rebind.
test_receiver:handledasserted for every accepted message type andevery length-guard rejection, including the subtle case that a
well-formed frame the service then refuses on policy (no
CAP_MIC) stillcounts as handled, because that is what separates
rx.malformedfrom apolicy drop.
test_session_service: audio counters cross-checked against the existingconcealment/reorder scenario (accepted, decoded, FEC-recovered and late
all agree with
submitMicAudioPcmcall counts) and thesilence-suppression scenario.
test_status_json: golden shape updated, plus a new test pinning thecounter blocks to
/api/debugand keeping them off/api/statusand the1 Hz SSE tick.
Page. Rendered headless (Chrome, DOM asserted, not just eyeballed)
against five payload shapes and three locales:
NaN/undefined, both backends still listed?lang=de,?lang=frAlso verified: the rate arithmetic (including the counter-reset and
zero-elapsed guards) and that navigating away clears both timers, with the
showViewwrappers indebug.jsandlogs.jschaining rather thanclobbering.
Not tested: an end-to-end run with a paired client streaming controller
audio, so the
audioblock's live values have not been observed againstreal traffic (the counters themselves are unit-tested against real Opus).
macOS and Linux builds were not run locally.
Checklist
clang-formatclean)