Skip to content

feat(moq-net): usage stats in the model (BroadcastInfo-carried, origin-attributed)#1895

Open
kixelated wants to merge 5 commits into
devfrom
claude/vigilant-shannon-t539t1
Open

feat(moq-net): usage stats in the model (BroadcastInfo-carried, origin-attributed)#1895
kixelated wants to merge 5 commits into
devfrom
claude/vigilant-shannon-t539t1

Conversation

@kixelated

@kixelated kixelated commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Implements the design in rs/moq-net/DESIGN-stats.md (PR #1894), which supersedes the with_meter / set_meter approach on #1873. Per-broadcast usage sinks live in BroadcastInfo, are set at construction, and ride the immutable Arc<BroadcastInfo> down to every track, group, and frame. Usage is atomics, so the model bumps through a shared &Arc<Usage> with no mutation, no setter, and no Arc::make_mut.

This takes over #1873 (which should be closed in favor of this).

What changed

The design's three phases, each landed as its own commit:

  1. The model carries usage stats. New model/usage.rs: Usage (groups/frames/bytes payload + opened/closed lifecycle atomics) and BroadcastStats (one Usage per direction). BroadcastInfo gains a stats field; Arc<BroadcastInfo> + Arc<TrackInfo> are threaded through Broadcast/Track/Group; payload bumps move into the model (ingress at create_group/append_frame, egress as the consumer reads). The stats-layer BroadcastStats is renamed BroadcastHandle.

  2. Origin attributes egress, ingress baked at construction. The lite/IETF subscriber loops build the broadcast with stats.producer already set. The per-session OriginConsumer::with_egress provider stamps each BroadcastConsumer it yields (announce stream, request_broadcast, dynamic accept) with that session's egress sink, so per-tier egress survives with zero mutation. The handler/gateway per-frame/byte/group bumps are deleted; the stats layer reads the shared Arc<Usage> instead of vending payload guards.

  3. Model-tracked live viewer/publisher counts. A Live refcount over each sink: a BroadcastConsumer is one live viewer while it has ≥1 outstanding TrackConsumer (the token rides into the TrackSubscriber, so a subscription counts for its whole life); a broadcast with ≥1 live TrackProducer is one publisher (the publisher token rides the shared broadcast state, so the relay's dynamic-accept ingress path is covered). SessionBroadcasts is gone; the publish loop maps opened - closed onto the existing broadcasts / broadcasts_closed fields, so the published schema is unchanged.

Semantics

  • Egress is per-consumer (N viewers count N times); ingress is single-writer (counts once). A relay counts both directions without double-counting either.
  • The lite fetch path is metered through the model: a fetch re-serves an existing group, so it bumps frames/bytes but not groups.
  • The model counts raw (decompressed) bytes vs. the old post-compression wire bytes. Only the hang catalog track sets compress, so the difference is noise; a follow-up will cache compressed bytes so the count is the wire size again. (Confirmed acceptable.)

Testing

cargo test -p moq-net passes (400 lib + 4 integration), including new model tests for ingress-once/egress-per-viewer, origin egress-stamping, and live viewer/publisher counts. clippy/doc/fmt clean; dependent crates (moq-mux, hang, moq-relay, moq-native, moq-srt, moq-json, moq-ffi) compile.

Note: this environment's egress policy blocks the kixelated/nvidia-video-codec-sdk git fork and the sandbox has no IPv6, so the full workspace just check and the moq-native end-to-end relay suite can't run here. CI's nix run is the source of truth for those.

Cross-package sync

  • js/net: not mirrored. The stats aggregator / usage sinks are relay-side Rust only; the browser client has no equivalent. No doc/concept wire change (this is API, not wire).

Breaking changes (target dev)

Public API in rs/moq-net: BroadcastInfo gains stats; Broadcast{Producer,Consumer,Dynamic} carry Arc<BroadcastInfo>; GroupProducer::new takes Arc<TrackInfo> + Arc<BroadcastInfo> (was Option<Timescale>); the stats-layer BroadcastStats is renamed BroadcastHandle; SessionBroadcasts / BroadcastSubscription and the publisher_broadcasts / subscriber_broadcasts / per-track frame/bytes/group methods are removed; Counters swaps its payload + broadcasts atoms for a shared Arc<Usage>. TrackProducer::new(name, info) keeps its signature (standalone tracks get a shared no-op broadcast).

Follow-ups

  • Wire moq-rtc (WHIP/WHEP) and moq-rtmp through the same origin egress / ingress-at-construction seam (the mechanism exists; their origins just need with_egress and an ingress-baked broadcast).
  • Cache compressed bytes in the model so the byte count is the wire size for compressed tracks.

🤖 Generated with Claude Code

(Written by Claude)

claude added 5 commits June 23, 2026 22:16
Move per-broadcast usage counting into the transport-agnostic model, the
foundation for metering every transport (moq-lite, IETF, and the non-MoQ
gateways) uniformly without re-instrumenting each data path.

New `model/usage.rs`: a `Usage` struct (groups/frames/bytes payload counters
plus opened/closed lifecycle counters, all atomics) and a `BroadcastStats`
pair (one `Usage` per direction). `BroadcastInfo` gains a `stats` field, so the
immutable broadcast handle carries the sinks down to every track, group, and
frame through a shared `Arc<BroadcastInfo>`. The group also carries an
`Arc<TrackInfo>`, so `timescale` is read from there instead of threaded
separately.

Producer-side handles bump the ingress sink (groups at create_group/
append_group, frames+bytes at append_frame); consumer-side handles bump the
egress sink as a subscriber receives groups and reads frames. A fetch
re-serves an existing group, so it bumps frames/bytes but not groups.

Behavior-preserving: the sinks default to unreferenced no-op atomics, so a
standalone broadcast is unmetered and the existing stats-layer wiring is
untouched. The stats-layer `BroadcastStats` is renamed `BroadcastHandle` to
free the name for the new model type. `TrackProducer::new` keeps its signature
(standalone tracks get the shared no-op broadcast).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tHLbSdh7JuEL8Uyahsd3d
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tHLbSdh7JuEL8Uyahsd3d
Wire the model's usage sinks to the stats layer and meter every transport
uniformly, removing the per-handler payload counting.

- Stats layer: each per-(tier, role) `Counters` holds an `Arc<Usage>` shared
  with the model (the same `Arc` baked into the broadcast), and the snapshot
  reads payload from it. `BroadcastHandle` vends the ingress/egress `Usage` for
  baking/stamping. The per-track `frame()`/`bytes()`/`group()` guard methods are
  gone; the track guards now track only the `subscriptions` lifecycle.
- Ingress baked at construction: the lite/IETF subscriber loops build the
  broadcast with `stats.producer` already set, so create_group/create_frame meter
  ingress. The handler's per-frame/byte/group bumps are removed.
- Egress attributed by the origin: `OriginConsumer::with_egress` carries a
  per-session sink provider; `get_broadcast`, the dynamic `request_broadcast`
  path, and `AnnounceConsumer` delivery stamp each `BroadcastConsumer`'s
  `stats.consumer` with the session's egress sink. The lite/IETF sessions attach
  it. The publisher's per-frame/byte/group bumps are removed; the model meters as
  the consumer reads (live via next_frame, fetch via get_frame; a fetch counts
  frames/bytes but not groups).

Egress is per-consumer (N viewers count N times) and ingress is single-writer.
Per the design, the model counts raw (decompressed) bytes; only the catalog track
compresses, so the difference is noise (a follow-up will cache compressed bytes).

Tests: model-level ingress-once/egress-per-viewer and origin egress-stamping; all
400 moq-net lib tests pass. Dependent crates compile; clippy/doc/fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tHLbSdh7JuEL8Uyahsd3d
Move the live viewer/publisher count into the model and retire the stats-layer
`SessionBroadcasts` sentinel. The published `broadcasts` / `broadcasts_closed`
schema is unchanged; it's now driven from a cleaner place.

- `usage.rs`: a `Live` refcount over a `Usage` sink. The `0 -> 1` transition
  bumps `opened`, the last `1 -> 0` drop bumps `closed`, so `opened - closed` is
  the live count. Clones of a handle share one `Live` (one logical viewer/publisher).
- Consumer side: `BroadcastConsumer` carries a viewers `Live` over its egress
  sink (re-keyed when the origin stamps the session sink). Each `TrackConsumer`
  from `track()` holds a token; it propagates to the `TrackSubscriber` so a
  subscription counts as a viewer for its whole life, not just while the
  `TrackConsumer` handle exists. N tracks on one consumer is still one viewer.
- Producer side: a publishers `Live` over the ingress sink lives on the shared
  broadcast state, so every track-creation path (`create_track`, `reserve_track`,
  and the dynamic handler's `requested_track` -> `accept`, which is the relay's
  ingress path) takes a token. A broadcast with >= 1 live track is one publisher.
- `stats.rs`: `Counters` drops its `broadcasts`/`broadcasts_closed` atoms and
  reads them from the model's `Usage.opened/closed` in the snapshot.
  `SessionBroadcasts` / `BroadcastSubscription` and the `publisher_broadcasts` /
  `subscriber_broadcasts` handle methods are removed.
- handlers: drop the `SessionBroadcasts` plumbing; the model counts viewers
  (egress) and publishers (ingress) automatically.

Tests: model-level live-count test (publisher per broadcast, viewer dedup across
tracks) plus the reworked snapshot-mapping test. 400 moq-net lib tests pass;
clippy/doc/fmt clean; dependent crates compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tHLbSdh7JuEL8Uyahsd3d
@kixelated kixelated changed the title feat(moq-net): usage stats in the model (BroadcastInfo-carried) feat(moq-net): usage stats in the model (BroadcastInfo-carried, origin-attributed) Jun 23, 2026
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.

2 participants