feat(moq-net): per-hop, per-frame payload compression (drafts#39)#1874
Open
kixelated wants to merge 4 commits into
Open
feat(moq-net): per-hop, per-frame payload compression (drafts#39)#1874kixelated wants to merge 4 commits into
kixelated wants to merge 4 commits into
Conversation
Compressible tracks (TrackInfo.compress) are Deflate-coded on the lite-05+ wire. The model previously inflated every frame on ingress and re-deflated it per egress connection, so a relay burned CPU decompressing and recompressing identical bytes for each downstream peer, and its in-RAM byte count tracked the decompressed size rather than what it actually stores and sends. Cache the bytes verbatim instead. The lite subscriber stores frames in the codec they arrived in, recorded on the new Frame::compression field. This is distinct from the compress egress hint: an origin marks a track compress but writes plaintext, so its cached frames stay None. Egress then: - forwards verbatim when the cached codec matches the wire codec (the common relay to modern-peer path: no inflate or deflate), and - recodes only when they differ: an origin compressing its plaintext, or a compression-incapable peer (old lite draft, IETF moq-transport, or an in-process consumer) that needs plaintext. FrameConsumer::read_all decodes against Frame::compression, so application consumers (the moq-json catalog reader, etc.) and the IETF / old-lite egress paths keep getting plaintext unchanged. read_chunk streams the stored bytes verbatim for the relay passthrough. The wire format is unchanged. The model's natural byte count now equals the compressed/wire count, which is what usage billing should meter. The old-client-decompress path undercounts egress (counts compressed, sends decompressed), an accepted tradeoff over overcounting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the moq-lite half of moq-dev/drafts#39: the publisher only signals *whether* a track is worth compressing, while the algorithm is negotiated per hop and recorded per frame. A single frame can opt out — e.g. a small JSON merge-patch delta that DEFLATE would only enlarge — instead of bloating it. Wire (moq-lite-05-wip): - TRACK_INFO `Publisher Compression` becomes a boolean hint (>1 reserved, decodes as true so it stays additive). It names no algorithm. - New SETUP `Compression` parameter (id 0x3): each endpoint advertises the algorithms it can decompress, packed varints, none(0) omitted. moq-net advertises [deflate]. Per hop, per direction; never forwarded by a relay. - New per-frame `Compression` field in FRAME, present iff the track is hinted, after the timestamp delta and before the length. Names the codec actually used (none/deflate); it doubles as the group's end-of-frames sentinel when no timestamp precedes it. Behavior: - The publisher reads the peer's advertised algorithms (PeerSetup) before serving a compress-hinted track. It forwards an already-DEFLATE cached frame verbatim when the peer can inflate it (relay passthrough, no inflate/deflate), and otherwise picks per frame whether DEFLATE actually shrinks the payload, sending `none` when it wouldn't (or the peer can't inflate). Empty payloads always use `none`. - The subscriber decodes the per-frame field and caches each frame in that codec, so the cache holds compressed bytes (billing counts the wire size) and inflates only at delivery. No datagram path exists in moq-net yet, so that part of the draft is N/A here. The IETF moqt compression extension (moq-compression) is left for a follow-up; the IETF path keeps sending verbatim, which is spec-compliant "extension not negotiated". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keeps the browser client interoperable with the updated relay on the lite-05-wip wire (the catalog is a `compress` track, so a stale browser would otherwise misparse it): - TRACK_INFO `Publisher Compression` becomes a boolean hint (`track.ts`). - New SETUP `Compression` parameter (`setup.ts`): we advertise `[deflate]`; the parameter is packed varints, none(0) omitted, unknown ids ignored on decode. - Per-frame `Compression` field: the subscriber reads it (gated on the hint) and decodes each frame by its own codec; the publisher writes it and picks DEFLATE per frame only when the peer advertised it and it shrinks the payload. The browser stays a consumer (decompress on ingress), so it doesn't adopt the relay's store-compressed model; it only needs to parse the new wire. The publisher reads the peer's SETUP (threaded from Connection) before compressing egress. Also refreshes the stale "negotiated in SUBSCRIBE_OK" wording in both compression module docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e None variant `Compression::None` was an awkward "compression that doesn't compress." Verbatim transfer is the *absence* of a codec, so model it as `Option::None` (Rust) / `undefined` (JS) and leave the enum with just real codecs (`Deflate`). This makes "compress with nothing" unrepresentable and stops a negotiated algorithm list from carrying a meaningless none. - `Compression` enum loses `None`; `compress`/`decompress` take `self` and only do real work (callers handle the verbatim case via the `Option`). - `from_code` returns `Option<Compression>` (`0` -> `None`); `to_code` is always non-zero. `is_none()` is dropped in favor of `Option::is_none`. - `Frame::compression` is now `Option<Compression>`; the wire/SETUP/per-frame-field code threads the `Option` (e.g. `codec.map_or(0, Compression::to_code)`), and the SETUP list is naturally `Vec<Compression>` with no none to filter. - JS mirrors it: `Compression` drops `None`, "no codec" is `undefined`. Pure internal refactor: the wire bytes (codes 0/1) are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 tasks
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.
Implements the moq-lite half of moq-dev/drafts#39: the publisher only signals whether a track is worth compressing, while the algorithm is negotiated per hop and recorded per frame. A single frame can opt out — e.g. a small JSON merge-patch delta that DEFLATE would only enlarge — and a relay forwards an already-compressed frame untouched instead of inflating/deflating it per downstream peer.
This supersedes the original "cache compressed bytes" idea (first commit, kept as the foundation): the model now stores frames in whatever codec each one names, and decodes only at delivery.
Wire changes (moq-lite-05-wip)
Publisher Compression→ a boolean hint (0/1;>1reserved, decodes astrueso it stays additive). Names no algorithm.Compressionparameter (id0x3): each endpoint advertises the algorithms it can decompress (packed varints,none=0 omitted), in preference order. Per hop, per direction; never forwarded by a relay. Both Rust and JS advertise[deflate].Compressionfield in FRAME, present iff the track is hinted, after the timestamp delta and before the length. Names the codec used (none/deflate); it doubles as the group's end-of-frames sentinel when no timestamp precedes it.No datagram path exists in moq-net yet (that part of the draft is N/A here).
Behavior
FrameConsumer::read_all);read_chunkstreams the stored bytes verbatim for the relay path.PeerSetup) before serving a hinted track. It forwards an already-DEFLATE cached frame verbatim when the peer can inflate it (relay passthrough — no inflate/deflate), and otherwise picks per frame whether DEFLATE actually shrinks the payload, sendingnonewhen it wouldn't (or the peer can't inflate). Empty payloads always usenone.compressbut writes plaintext, so its cached frames stayNoneand the publisher compresses on egress; a relay caches whatever its upstream sent and forwards/decodes per frame. Both are correct because the codec lives per-frame onFrame::compression, distinct from thecompresshint.Cross-package (Rust + JS, kept in sync)
rs/moq-net— the reference implementation (above).js/net— mirrors the wire so the browser stays interoperable (the catalog is acompresstrack). The browser is a consumer, so it keeps decompressing on ingress and just parses the new wire: boolean hint, SETUPCompressionparam, per-frame field; its publisher reads the peer's SETUP and picks DEFLATE per frame only when it shrinks.doc/— no existing compression page to update (the spec lives in the drafts repo); refreshed the stale "negotiated in SUBSCRIBE_OK" wording in bothcompressionmodule docs.moq-compressionextension: left for a follow-up; the IETF path keeps sending verbatim (spec-compliant "extension not negotiated").Public API changes — breaking, targets
devrs/moq-net:Framegainscompression: Compression;FrameConsumer::read_allreturns the decoded payload (was verbatim) whileread_chunkstays verbatim;Compression::is_noneadded;lite::TrackInfo.compression→compress: bool;lite::Setupgainscompression: Vec<Compression>+PeerSetup::compression().js/net:liteTrackInfo.compression→compress: boolean;Setupgainscompression: Compression[](both internal to thelitemodule).Test plan
None), both inflating back correctly; moq-relay (129);cargo check --workspace --tests,fmt+clippy -D warnings+cargo doc(via nix) — clean.@moq/net192 tests (incl. new SETUPCompressionparam round-trip + skip-none/unknown and TRACK_INFO additive-hint),tsc --noEmitclean, Biome clean;@moq/hangtypecheck clean.🤖 Generated with Claude Code
(Written by Claude)