From cd585bee870a9b2b1e806afc059d250a70ff9f3b Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:43:41 -0700 Subject: [PATCH 1/5] claim: stats prefix tracks From 859d02d1198bc7501e0bfe384819f0eebfd11db9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:45:28 -0700 Subject: [PATCH 2/5] quest: surface stats prefix wire decisions Co-Authored-By: GPT-5.4 --- quest/m1/stats-prefix-tracks.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/quest/m1/stats-prefix-tracks.md b/quest/m1/stats-prefix-tracks.md index 42dacdccd2..411fb5800a 100644 --- a/quest/m1/stats-prefix-tracks.md +++ b/quest/m1/stats-prefix-tracks.md @@ -72,6 +72,36 @@ This supersedes the 2026-10-05 plan to retire the maps in the same release. Public API: `moq-stats` producer and consumer types. Wire: stats track names and payloads. +### Open decisions + +Implementation is blocked on the public wire shape. Recommended choices are +listed first so the draft can resume without rediscovery: + +1. **One namespaced track per prefix (recommended):** + `prefix//traffic.json[.z]`, with tiers and both roles in the payload. + The fixed `prefix/` namespace makes path and tier labels unambiguous and one + subscription returns the complete prefix view. Alternatives are one track + per tier and role (more subscriptions and an escaping rule) or percent/base64 + encoding either the tier or path (opaque names and another canonicalization + contract). +2. **Keep both plain and compressed siblings (recommended):** match every + existing stats track and let old/simple readers use JSON while fleet readers + use the compressed snapshot. The alternative removes plain JSON for this new + track, reducing producer work but making inspection and consumers special + cases. +3. **Do not truncate or page children initially (recommended):** benchmark the + complete map against fan-out and rely on the existing hard frame-size error + to fail loud. A deterministic cap silently makes the rollup disagree with + its visible children; paging adds ordering, snapshot-consistency, and request + lifecycle semantics before measurements show they are needed. + +After these are decided, put held-prefix accumulation in `moq-net`'s registry +rather than walking every report row per held prefix in `moq-stats`: each +`(path, tier)` entry should cache the currently held ancestors and child slots, +so payload bumps stay lock-free and scale with path depth. The publisher owns +the request lifecycle and serializes those snapshots; the aggregator applies +its existing per-node reset baseline to each cumulative prefix value. + ## Related - [Viewer lag histogram](/quest/m1/qos/lag-histogram.md) - lands on the totals From 40188e614ed818f942d2aa8a192bb0169d3b768b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 09:35:03 -0700 Subject: [PATCH 3/5] quest: settle stats prefix track name, payload, and encodings Co-Authored-By: Claude Opus 5.5 --- quest/m1/stats-prefix-tracks.md | 58 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/quest/m1/stats-prefix-tracks.md b/quest/m1/stats-prefix-tracks.md index 411fb5800a..e497c54cf6 100644 --- a/quest/m1/stats-prefix-tracks.md +++ b/quest/m1/stats-prefix-tracks.md @@ -38,8 +38,7 @@ Decided 2026-10-05 and 2026-10-07: - A request for a prefix with no members is held open at zero and reclaimed when its last consumer leaves. - A child leaves the one-level map after the frame carrying its closing - readout; its tail stays in the parent's rollup. Decide while implementing - whether a map with hundreds of children needs a cap or paging. + readout; its tail stays in the parent's rollup. - **Self counters.** A prefix track also carries cumulative counters for members at exactly the prefix, so a reader can show a broadcast that also has nested broadcasts. A reader can't derive them as the rollup less the current @@ -51,7 +50,8 @@ Decided 2026-10-05 and 2026-10-07: upstream (node, epoch) hold and treats a decrease as that upstream's reset, so a merged prefix track never regresses within one downstream subscription. Test a join, a leave, and a restart under a held prefix. -- **Hot path.** Keep it lock-free: a held prefix registers atomic +- **Hot path.** Reopened in #5719: summing held prefixes per drain in + `moq-stats`, as presence does, may replace this. Keep it lock-free: a held prefix registers atomic accumulators, each entry caches its held ancestors (and the matching child slot in each), and an update bumps them in a loop, about twice the held depth per update. Registration and release reach existing entries without a @@ -61,46 +61,34 @@ Decided 2026-10-05 and 2026-10-07: - Held prefixes have no cap: a hold is reclaimed when its last reader leaves, like presence tracks, whose cap was dropped for that reason (decided 2026-10-12 in #5084). -- Decide whether prefix tracks keep `.json.z` siblings. Update the - aggregator, `doc/concept/stats.md`, and `doc/bin/inspect.md`. +- Update the aggregator, `doc/concept/stats.md`, and `doc/bin/inspect.md`. Decided 2026-10-12 (#5084): the map tracks are kept, not retired. Prefix tracks are added beside them so existing readers keep working, the same backwards-compatible shape stats-split took when it kept `sessions.json`. This supersedes the 2026-10-05 plan to retire the maps in the same release. -Public API: `moq-stats` producer and consumer types. Wire: stats track names -and payloads. - -### Open decisions +Decided 2026-10-16 (#5719): -Implementation is blocked on the public wire shape. Recommended choices are -listed first so the draft can resume without rediscovery: +- **Name.** `/traffic.json`, where `` is the full broadcast + prefix, as presence takes the full root, and a prefix outside the group is + refused. The last segment picks the kind and everything before it is the + key, like `/presence.json`, so the name is injective without a + namespace segment. Tiers ride the payload, never the name, so one + subscription returns the whole prefix and no escaping rule is needed. +- **Payload.** Keyed by tier like `totals.json`. Each tier entry carries the + rollup's `publisher` and `subscriber` at its top level, so a totals reader + reads the rollup unchanged, plus `self: {publisher, subscriber}` and + `children: {"": {publisher, subscriber}}`, keyed by the child's + next path segment. +- **Encodings.** Plain and `.json.z` siblings, like every other stats track. +- **No cap or paging on children.** `publisher.json` already carries every + path in the group uncapped and fails loud with `FrameTooLarge`; a prefix's + child map is a subset of that frame. A cap would make the rollup disagree + with its visible children. -1. **One namespaced track per prefix (recommended):** - `prefix//traffic.json[.z]`, with tiers and both roles in the payload. - The fixed `prefix/` namespace makes path and tier labels unambiguous and one - subscription returns the complete prefix view. Alternatives are one track - per tier and role (more subscriptions and an escaping rule) or percent/base64 - encoding either the tier or path (opaque names and another canonicalization - contract). -2. **Keep both plain and compressed siblings (recommended):** match every - existing stats track and let old/simple readers use JSON while fleet readers - use the compressed snapshot. The alternative removes plain JSON for this new - track, reducing producer work but making inspection and consumers special - cases. -3. **Do not truncate or page children initially (recommended):** benchmark the - complete map against fan-out and rely on the existing hard frame-size error - to fail loud. A deterministic cap silently makes the rollup disagree with - its visible children; paging adds ordering, snapshot-consistency, and request - lifecycle semantics before measurements show they are needed. - -After these are decided, put held-prefix accumulation in `moq-net`'s registry -rather than walking every report row per held prefix in `moq-stats`: each -`(path, tier)` entry should cache the currently held ancestors and child slots, -so payload bumps stay lock-free and scale with path depth. The publisher owns -the request lifecycle and serializes those snapshots; the aggregator applies -its existing per-node reset baseline to each cumulative prefix value. +Public API: `moq-stats` producer and consumer types. Wire: stats track names +and payloads. ## Related From 2ba12b42dda5d258aac43aced6c2a1560a7eb523 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 09:44:44 -0700 Subject: [PATCH 4/5] quest: sum stats prefix tracks per drain like presence Co-Authored-By: Claude Opus 5.5 --- quest/m1/stats-prefix-tracks.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/quest/m1/stats-prefix-tracks.md b/quest/m1/stats-prefix-tracks.md index e497c54cf6..0a9fc7cca0 100644 --- a/quest/m1/stats-prefix-tracks.md +++ b/quest/m1/stats-prefix-tracks.md @@ -26,15 +26,13 @@ Decided 2026-10-05 and 2026-10-07: Nothing is produced for a prefix no one holds. The relay's `--stats-depth` only places the group broadcasts; requests are not capped by it. Track names must stay injective: tiers and paths both contain `/`. -- Flow counters (bytes, frames, groups, fetches) count from zero when the - prefix is first held and fold in members that end while it is held. Their - absolute values are not authoritative, so readers take differences within - one subscription; the group totals carry the real sums. -- Started/ended pairs (announces, broadcasts, subscriptions) are seeded at - registration: `started` with each current member's `started - ended`, - `ended` at zero, so every `active` stays exact for a prefix held - mid-stream. Test a prefix held while 300 subscriptions are live, then watch - them leave. +- Counters follow presence (revised 2026-10-16, #5719): each drain sums the + live members' readouts plus the closing readouts of members that ended + while the prefix was held. Live members carry their history from before + the hold, so absolute values are not authoritative and readers take + differences within one subscription; the group totals carry the real sums. + Every `active` (started less ended) is exact for a prefix held mid-stream. + Test a prefix held while 300 subscriptions are live, then watch them leave. - A request for a prefix with no members is held open at zero and reclaimed when its last consumer leaves. - A child leaves the one-level map after the frame carrying its closing @@ -50,14 +48,14 @@ Decided 2026-10-05 and 2026-10-07: upstream (node, epoch) hold and treats a decrease as that upstream's reset, so a merged prefix track never regresses within one downstream subscription. Test a join, a leave, and a restart under a held prefix. -- **Hot path.** Reopened in #5719: summing held prefixes per drain in - `moq-stats`, as presence does, may replace this. Keep it lock-free: a held prefix registers atomic - accumulators, each entry caches its held ancestors (and the matching child - slot in each), and an update bumps them in a loop, about twice the held - depth per update. Registration and release reach existing entries without a - lock or a walk of every entry, for example an `ArcSwap` ancestor list or a - generation counter rechecked on bump. Benchmark across held-prefix count and - depth. +- **Drain-time sums** (revised 2026-10-16, #5719). Sum held prefixes in + `moq-stats` per drain with `Cumulative`, like presence: walk each report + row's ancestors and look each up in the held set, so the cost scales with + rows times path depth, not held prefixes, and nothing touches the per-frame + path. No `moq-net` API change: registry-level prefix accumulators would + push request lifecycle into the collection layer and put shared atomics on + every frame bump under a held prefix. Extend `producer_tick` to sweep rows, + held prefixes, and depth. - Held prefixes have no cap: a hold is reclaimed when its last reader leaves, like presence tracks, whose cap was dropped for that reason (decided 2026-10-12 in #5084). From b2d49abe9b442ae290e46271e3c928a7c4aa44ef Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 10:07:25 -0700 Subject: [PATCH 5/5] quest: require stats prefix tracks to refuse oversized frames Co-Authored-By: Claude Opus 5.5 --- quest/m1/stats-prefix-tracks.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/quest/m1/stats-prefix-tracks.md b/quest/m1/stats-prefix-tracks.md index 0a9fc7cca0..590a80a3b1 100644 --- a/quest/m1/stats-prefix-tracks.md +++ b/quest/m1/stats-prefix-tracks.md @@ -80,10 +80,14 @@ Decided 2026-10-16 (#5719): `children: {"": {publisher, subscriber}}`, keyed by the child's next path segment. - **Encodings.** Plain and `.json.z` siblings, like every other stats track. -- **No cap or paging on children.** `publisher.json` already carries every - path in the group uncapped and fails loud with `FrameTooLarge`; a prefix's - child map is a subset of that frame. A cap would make the rollup disagree - with its visible children. +- **No cap or paging on children.** A cap would make the rollup disagree with + its visible children, and paging adds ordering and consistency rules no + measurement asks for yet. A child entry carries both roles per tier, so the + frame reaches the 32 MB `MAX_CACHE_BYTES` limit sooner than `publisher.json` + does (roughly ten thousand children across four tiers), far past the + hundreds a channel holds. An oversized frame must fail loud: end the prefix + track with `FrameTooLarge` instead of the existing tracks' debug log, which + leaves readers on a stale snapshot. Test a prefix whose frame overflows. Public API: `moq-stats` producer and consumer types. Wire: stats track names and payloads.