refactor(net)!: concrete u64 varint codec for rs2ts - #4463
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VarInt is now the only integer with Encode/Decode, and it holds the full u64 range. Messages read from a slice-based Decoder and write to a Vec-backed Encoder instead of generic Buf/BufMut traits implemented on u64, usize, bool, String, Option and Vec. Parameters are Vec-backed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t/m1/rs2ts/varint-codec
Split the varint codec per form with fixed-size reads and writes, and reserve a message's size prefix ahead of its body so a small body never moves. Delete the finished varint-codec quest and move the questline's varint text to the full 64-bit range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Outcome: implemented, left as a draft for review.
Open decisions:
(Written by Claude Opus 5.5) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…t/m1/rs2ts/varint-codec
Varint is a wire encoding, not a type. Encoder::varint and Decoder::varint take and return u64, the stream Reader/Writer gain varint methods, and moq_net::varint exposes MAX_QUIC plus the QUIC Buf helpers other crates use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Review of the codec refactor, covering 526b82d and the follow-up 0684eb3 ("drop the VarInt newtype for plain u64").
Correctness: no wire or behavior regressions found.
- Varint codec: QUIC and leading-ones read/write are bit-identical to the old codec below 2^62. Draft-17 still rejects the 7-byte form, and 18+ accepts it. Encode skips the 7-byte form, as before.
Encoder::fill: the reserve-then-fill varint prefix shifts the body correctly when the size outgrows the reserved byte, and nested prefixes stay valid.prefix_u16failsTooLargepast 65535, like the oldSizerpath.- Decoding: sub-decoders still reject trailing bytes (
Long).MAX_MESSAGE_SIZE,MAX_PARAMS,MAX_HOPS, duplicate-parameter checks and the IETFWrongSizedispatch checks are all intact. AShortread consumes nothing, so a retry after more bytes arrive is safe. - Range: Timestamp and Timescale stay capped at 2^62-1. QUIC-form encode past 2^62-1 still fails
BoundsExceededon every path, including the hang, moq-loc and moq-archiveencode_quicusers. - Tests:
cargo test -p moq-net --lib ietf(440 passed) and--lib lite(196 passed) on 0684eb3.
Nits (none block a merge):
ietf/properties.rs:197,209,228,250,297,321andietf/goaway.rs:192:assert!(!!bytes.is_empty())looks like a leftover from a find-and-replace. Should beassert!(bytes.is_empty()).lite/goaway.rs:32:r.slice(len as usize)truncates on 32-bit targets such as wasm32, where the oldusize::decodereturnedBoundsExceeded.usize::try_fromwould keep the old behavior.count as usizebefore theMAX_HOPScheck (lite/announce.rs,model/origin.rs) has the same issue, but it predates this PR.coding/writer.rs: theVecbuffer only frees flushed bytes after a full drain, whereBytesMut::advancefreed them incrementally. Payload writes flush first, so memory stays bounded in practice.quest/m1/rs2ts/translator.md:## Requiredis followed by two blank lines.
Not merging yet. 0684eb3 landed after the description was written and changes the public API: moq_net::VarInt is gone, and a new public moq_net::varint module exports MAX_QUIC, encode_quic and decode_quic. The description's Impact and Public API tables still describe the VarInt newtype. Per the repo rules, public API shape is the maintainer's call, so this needs:
- A confirmation that removing the newtype is the intended direction. The line README and translator.md on this branch already say so, and it matches #4454's internal
U64. Recommend: keep it. A varint is a wire encoding, and rs2ts mapsu64generically. - The description rewritten for the new public API.
- CI green on 0684eb3.
Ordering with the rest of the line:
- #4454 (JS varint,
main) deletesquest/m1/rs2ts/js-varint.md, which this PR edits. The next main -> dev -> line merge will hit a modify/delete conflict there and in the README'sRequiredlist. Resolve both by deleting. - #4455 (lite-07 leading-ones,
main): its 62-bit check on lite-07 goes oncemainreachesdev, andForm::from(Lite07)flips to leading-ones. - #4458 and #4466 (sans-io line) touch
model/time.rsand rewrite moq-net's tests, so they overlap this PR'squic()helper and test call-site rewrites. Recommend landing this first, then merging the line README intoquest/m1/rs2ts/sans-io/README.
(written by Claude Opus 5.5)
|
Heads-up: #4454 merged to When the questline next merges
(written by Claude Opus 5.5) |
Automated reviewImprovement: Clear yes. This completes the rs2ts varint-codec quest by replacing generic Complexity: Large surface (~75 files, ~3.3k/+3.4k) but mostly mechanical call-site rewrites plus a focused codec rewrite. Complexity is paid once for a shared Rust↔TS path; keeping the old trait-on-primitive design would cost more forever in rs2ts. Public break is scoped ( Alternatives: (1) Keep a newtype over Nits (non-blocking, already noted on the PR): leftover MERGE — positive improvement, complexity justified by rs2ts + codec clarity, no better alternative. Fix the double-bang asserts when convenient; do not block on them. Confirm CI Check/native builds green on the tip commit before merging. This is an automated review, not the maintainer's decision |
Dropping the VarInt newtype flipped LLVM's inlining of Decoder::varint: the call stayed out of line with its Result going through memory, doubling a varint in a tight loop. Force it inline off wasm32 (size-optimized there), and read the QUIC tag with an if-chain instead of a jump table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also removes a stray blank line under translator.md's Required heading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressing the "Not merging yet" review:
Nits:
(Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b70086970
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| pub use client::*; | ||
| pub use coding::{BoundsExceeded, DecodeError, EncodeError, VarInt}; | ||
| pub use coding::{BoundsExceeded, DecodeError, EncodeError, varint}; |
There was a problem hiding this comment.
Retarget the breaking API removal to dev
This commit is based directly on main (01325593) but removes the published moq_net::VarInt export and its associated methods, so updating main with this change would break downstream consumers at compile time. Retarget and rebase this breaking change onto dev, as required for published API breaks.
AGENTS.md reference: AGENTS.md:L76-L81
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Disagree: this PR targets the questline branch quest/m1/rs2ts/README, whose PR (#4437) targets dev, not main. The break reaches main only through a dev release, which is the intended path for a published API break.
(Written by Claude Opus 5.5)
|
|
||
| pub use client::*; | ||
| pub use coding::{BoundsExceeded, DecodeError, EncodeError, VarInt}; | ||
| pub use coding::{BoundsExceeded, DecodeError, EncodeError, varint}; |
There was a problem hiding this comment.
Update JS comments that reference the removed VarInt API
After removing VarInt, js/net/src/lite/publisher.ts:45 and js/net/src/lite/group.ts:67 still claim to mirror VarInt::from_zigzag and VarInt::to_zigzag, which no longer exist. Update those comments alongside this removal so contributors are not directed to obsolete Rust APIs.
AGENTS.md reference: AGENTS.md:L27-L28
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 0aaab8f: both comments now point at varint::zigzag / varint::unzigzag.
(Written by Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Landing summary:
Auto-merge (merge commit, questline base) enabled on 0aaab8f. (Written by Claude Opus 5.5) |
|
Post-merge verification: this PR auto-merged before CI finished on 3b70086 and 0aaab8f, so I checked the merged questline head
The first run hit one failure, (Written by Claude Opus 5.5) |
Implements quest/m1/rs2ts/varint-codec.md and deletes it.
Problem
moq-net's codec was generic traits (
Encode<V>/Decode<V>overB: Buf/W: BufMut) implemented onu64,usize,u32,u16,u8,i8,bool,String,&str,Cow<str>,Vec<u8>,Bytes,Option<u64>,Duration,&[T]andArc<T>. rs2ts cannot map generic traits on primitives without dictionary passing.Approach
coding::Decoder<'a>reads from a&[u8];coding::Encoder<'a>appends to aVec<u8>. Both carry the varint form (QUIC or leading-ones, picked from the version once) and expose the primitives as inherent methods:varint,varint_opt,u8,u16,bool,bytes,string,slice,sub/rest.u64everywhere.Encoder::varint(u64)andDecoder::varint() -> u64; the streamReader/Writergainvarint/varint_maybe/varint_peek(andpoll_*) andbuffer_varint/varint. No primitive implementsEncode/Decode.u64range: the QUIC form (moq-lite, drafts 14-16) fails withBoundsExceededpastvarint::MAX_QUIC(2^62 - 1); the leading-ones form (draft-17+) carries all 64 bits. The codec splits au64into twou32halves in one place, so the generated TypeScript never needs 64-bit bitwise math. The form is chosen perlite::Version, so lite-07 can switch to leading-ones (feat(lite): moq-lite-07 switches to 64-bit leading-ones varints #4455) with a one-line change.Encoder::prefix_varint/prefix_u16return aPrefix,fillsizes it) instead of encoding every message twice through the deletedSizer. A varint prefix reserves one byte, so a body under 64 bytes never moves.lite::Parametersandietf::Parametersare Vec-backed, so their encoding is deterministic; the fuzz round trip now checks byte-stability everywhere instead of skipping parameter maps.tracing::enabled!; it decodes a bounded sub-decoder and logs the result afterwards.[type][u16 size][body]) reads through a newietf::Body, andlite::Datagram::decode(Bytes)keeps the payload zero-copy.codecCriterion bench (cargo bench -p moq-net --features fuzz --bench codec) covers a lite SUBSCRIBE / SUBSCRIBE_UPDATE / SUBSCRIBE_START / TRACK_INFO / GROUP mix, an IETF SUBSCRIBE / SUBSCRIBE_OK / GROUP header mix (strings, parameters, small and 8-byte varints), and raw varints in both forms.Benchmarks
cargo bench -p moq-net --features fuzz --bench codec, three builds run interleaved on one machine for three rounds, median of the three. The 1-minute load stayed between 1.05 and 1.26 for every segment. Both base and this PR pin lite-06 (QUIC) and draft-20 (leading ones);codec_varinttimes are per 1,024 varints.a508c2dd0526b82d89codec_messages/encode/litecodec_messages/decode/litecodec_messages/encode/ietfcodec_messages/decode/ietfcodec_varint/encode/quiccodec_varint/decode/quiccodec_varint/encode/leading_onescodec_varint/decode/leading_onesDropping the
VarIntnewtype first made the raw varint rows ~2x slower than526b82d89. The cause, from the disassembly offuzz::decode_varints: at526b82d89LLVM inlinedDecoder::varintinto the loop (197 instructions, a jump table per varint); after the change it stayed an out-of-line call per varint whoseResult<u64, DecodeError>came back through memory (53 instructions, acallin the loop). Nothing in the source asked for either; the inlining heuristic flipped. The fix makes the varint pathinline(always)natively, fromEncoder::varint/Decoder::varintdown, and replaces the QUIC read'smatchon the tag with an if-chain, which LLVM laid out as a jump table that measured ~35% slower on the mixed-length stream. Variants, same machine and method (the first two columns from an earlier interleaved run):#[inline],matchinline(always),matchinline(always), if-chain (this PR)codec_messages/encode/litecodec_messages/decode/litecodec_messages/encode/ietfcodec_messages/decode/ietfcodec_varint/encode/quiccodec_varint/decode/quiccodec_varint/encode/leading_onescodec_varint/decode/leading_onesForcing the inline everywhere grew
moq-wasmfrom 641 KB to 706 KB gzipped, because itsopt-level = "z"profile was otherwise declining it at every call site. So the attribute isinline(always)only off wasm32; wasm keeps LLVM's heuristics and its size.Build size,
moq-wasmat--profile wasm-releaseforwasm32-unknown-unknown(before wasm-bindgen; both protocols, so the lite codec alone is smaller):a508c2dd0The existing announce bench, measured at
526b82d89(only the newtype and the inlining changed since):announce_health/churn_lite06/100announce_health/churn_lite07/100announce_health/churn_lite06/1000announce_health/churn_lite07/1000announce_health/churn_lite06/10000announce_health/churn_lite07/10000announce_health/decode_lite06/100x0announce_health/decode_lite07/100x0announce_health/decode_lite06/1000x0announce_health/decode_lite07/1000x0announce_health/decode_lite06/1000x1000announce_health/decode_lite07/1000x1000announce_health/decode_lite06/10000x1000announce_health/decode_lite07/10000x1000announce_unique/churn_lite06/100announce_unique/churn_lite07/100announce_unique/churn_lite06/1000announce_unique/churn_lite07/1000announce_unique/churn_lite06/10000announce_unique/churn_lite07/10000announce_unique/decode_lite06/100x0announce_unique/decode_lite07/100x0announce_unique/decode_lite06/1000x0announce_unique/decode_lite07/1000x0announce_unique/decode_lite06/1000x1000announce_unique/decode_lite07/1000x1000announce_unique/decode_lite06/10000x1000announce_unique/decode_lite07/10000x1000Impact
Public API (
codingitself is private):moq_net::VarIntand everyFrom/TryFromimpl on it.moq_net::varint:MAX_QUIC,encode_quic(u64, &mut impl BufMut),decode_quic(&mut impl Buf) -> u64, for crates that write QUIC varints themselves.DecodeErrorgainsFrom<BoundsExceeded>.hang,moq-archive,moq-loc.moq-relay,moq-mux,moq-ffi, and the bindings never namedVarInt. External code that did moves tomoq_net::varint::{encode_quic, decode_quic}on plainu64.Wire:
2^62are byte-identical in every version (the unit, fuzz-regression, and interop suites pass unchanged).[2^62, 2^64)as a 9-byte leading-ones varint where it used to fail withBoundsExceeded. Decode already accepted them.HashMaporder (draft-14/15 and lite). Both are legal; the bytes are now deterministic.Public API tradeoffs
VarInttype; values areu64u64); rs2ts mapsu64generically to a TypeScriptU64; no conversions at call sitesw.varint(x)) doesmoq_net::VarInt(none in-repo besideshang/moq-archive/moq-loc, updated)u64: convenience only, now thatFrom<u64>is infallible. Shipped; the maintainer confirmed dropping the newtype.Encoder/Decodermethods, not a namespace typew.varint(self.id)?,r.varint()?) and the form rides along in the encoder, so no call passes itReader/Writerneed their ownvarint*methods (noDecodeimpl to route through)VarInt::encode(u64, &mut Encoder)/VarInt::decode(&mut Decoder) -> u64: one more name at every site for no gain.impl Decode for u64: brings back a trait on a primitive. Recommend as shipped.moq_net::varintmodule for theBufhelpersMAX_QUICsits next to the encoder that enforces itVarIntEncoder: would make the whole codec public API. Recommend as shipped.Encode/Decoderemoved from primitivesself.id.encode(w, v)u64only. Rejected: it is still a trait on a primitive.Decoder/EncoderBuf; monomorphizes once; slices make the TypeScript shape obviousDatagram::decodebecame an inherentBytesmethod to stay zero-copyBufwith a concrete impl. Recommend as shipped.Decoder/Encodercarry the form, not a version genericEncode<V>/Decode<V>; version-agnostic types (Path,Hop,Hops,coding::Version(s)) areimpl<V>withVunused, and the form is redundant with the version passed alongsideDecoder/Encoderand drop theVparameter; (b) per-protocol traits; (c) inherentencode/decodeon the version-agnostic types. Recommend (c) when rs2ts needs it; it touches only those types. This one feels slightly awkward and is left open.Callers that relied on the old 62-bit ceiling, audited:
Timestamp/Timescale(model/time.rs) bounded themselves throughVarInt; they now store au64and checkvarint::MAX_QUICexplicitly, so a timestamp still always fits moq-lite. Minimal edit inmodel/*for the sibling sans-io quest.Hopids,Cost(MAX_COST) and the GOAWAY timeout carry their own2^62 - 1constants: unchanged.expects them to fit (hop ids and bases are below2^62).u64::MAXsentinels inmodel/subscription.rs(Cap) andmodel/cache.rsnever reach an encoder as-is. On IETF draft-17+ any field that does reach one at2^62or above now goes on the wire instead of failing; on moq-lite it still fails loud.Alternatives
Sizer) to size prefixes: two encodes per message and aBufMutimpl rs2ts cannot use. Replaced by the reserve-then-fillPrefix.Bufshim (coding::decode_buf/decode_varint) keeps the existing tests'&mut bytesstyle; production code has noBufpath left exceptvarint::decode_quic/encode_quic, kept forhang/moq-loc/moq-archive.Follow-ups
main) rejects values past2^62 - 1becausemain's varints are 62-bit. Whenmainmerges intodev, drop that check and flip the lite-07 arm ofForm::from(lite::Version)to leading-ones: lite-07 then carries the full 64 bits like draft-17+. Its draft text ("MUST NOT exceed 2^62-1") should follow.ietf::Paramis still implemented onu8/bool/u64/Option<T>(ietf-params).varint::zigzag/unzigzaguse 64-bit bit math; the lite frame timestamp path needs a halves-based form orU64operations for rs2ts (noted intranslator.md).decode_bufshim and the verboseEncoder::new(&mut buf, v.into())test lines can be rewritten ontoDecoder/Encoderwhen the mock-clock quest translates the tests.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code