From c0269859b3436350a522acc4833a6e54477fce27 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 14:26:36 +0000 Subject: [PATCH] nestsClient: revert framesPerGroup default to 1 now that the QUIC fix lands frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production sweep_frames_200 confirms received=200/200 missing=[] after the :quic MAX_STREAMS_UNI extension landed in d391ae1d. The framesPerGroup=5 mitigation in NestMoqLiteBroadcaster (commit 10ad69f1) is no longer needed and was a workaround for a real QUIC bug; reverting to 1 brings the wire shape in line with the JS reference broadcaster (one Opus frame per moq-lite group → late-join initial gap drops from ≤100ms back to ≤20ms). Tests that still want one-frame-per-group semantics keep their explicit framesPerGroup=1 overrides — they pin the assumption independent of any future default change. The broadcaster's framesPerGroup field kdoc is updated to explain the history (mitigation → root-caused fix → revert). All :quic and :nestsClient JVM tests pass. Plan doc: nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md status flipped to CLOSED with the production-validation numbers. --- ...6-05-01-quic-stream-cliff-investigation.md | 14 ++++-- .../audio/NestMoqLiteBroadcaster.kt | 47 +++++++++---------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md b/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md index 5bb337c87..6a048b4fb 100644 --- a/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md +++ b/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md @@ -1,7 +1,7 @@ # QUIC stream cliff against nostrnests.com — investigation plan -**Status: ROOT-CAUSED + FIXED.** Our `:quic` client never emitted -`MAX_STREAMS_*` frames to extend the peer-initiated stream-id cap. +**Status: CLOSED.** Our `:quic` client never emitted `MAX_STREAMS_*` +frames to extend the peer-initiated stream-id cap. [QuicConnectionConfig.initialMaxStreamsUni] (default `100`) was the *lifetime* maximum the peer could ever open. The relay forwards each broadcast group as a fresh peer-initiated uni stream, so any @@ -11,9 +11,13 @@ Fixed by tracking [QuicConnection.peerInitiatedUniCount] and emitting peer's usage crosses the half-window threshold — same pattern as the existing `MAX_DATA` / `MAX_STREAM_DATA` extension. -The `framesPerGroup = 5` mitigation in [NestMoqLiteBroadcaster] can -be reverted to `1` to match the JS reference broadcaster's wire -shape now that the underlying QUIC bug is gone. +Production validation: `sweep_frames_200` against `nostrnests.com` +went from `received=99/200 missing=[99-199]` (pre-fix) to +`received=200/200 missing=[]` (post-fix). The +[NestMoqLiteBroadcaster.DEFAULT_FRAMES_PER_GROUP] mitigation has +been reverted from `5` back to `1` so the broadcaster matches the +JS reference's wire shape (one Opus frame per moq-lite group → ≤ 20 ms +late-join initial gap). **Reproduces against:** `https://moq.nostrnests.com:4443` (production relay). Not consistently reproducible against the local diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt index 91319b5ba..2bbd865c2 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt @@ -49,26 +49,27 @@ class NestMoqLiteBroadcaster( * varint-prefixed frames; [MoqLitePublisherHandle.endGroup] FINs * the stream and the next [send] starts a fresh group. * - * **Default = 5 (≈ 100 ms of audio per group).** This is a - * mitigation for a production loss cliff observed against - * `nostrnests.com`: at one-frame-per-group and 20 ms cadence the - * client opens 50 uni streams/second, and somewhere around the - * 99th stream the connection enters a state where new uni streams' - * data stops being delivered to the listener, even though - * `publisher.send` still returns true. Packing 5 frames per group - * reduces the stream-creation rate to 10/second, which the prod - * sweep (`NostrnestsProdAudioTransmissionTest.sweep_frames_per_group_5`) - * shows delivers 100/100 frames cleanly. See + * **Default = 1 (one Opus frame per group).** Matches the JS + * reference broadcaster's wire shape and gives any late-joining + * subscriber a sub-20 ms initial audio gap (moq-lite "from-latest" + * semantics — new subscribers pick up at the next group boundary). + * + * Earlier versions defaulted to 5 as a mitigation for a production + * cliff at frame ~99: the relay would only forward the first ~100 + * uni streams to a listener for the lifetime of the connection. + * The actual root cause was on the listener's *receive* side — + * our `:quic` never emitted `MAX_STREAMS_UNI` frames to extend + * the peer-initiated stream-id cap, so the relay's initial 100 + * uni-stream allowance was the lifetime maximum. Once + * [com.vitorpamplona.quic.connection.QuicConnectionWriter] + * started emitting periodic `MAX_STREAMS_*` extensions, packing + * stopped being necessary. See * `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md`. * - * Trade-off: a brand-new subscriber that attaches mid-broadcast - * picks up at the next group boundary per moq-lite "from-latest" - * semantics. With 5 frames per group, late joiners miss up to - * 100 ms of audio. With the original 1 frame per group it was - * up to 20 ms. Both are imperceptible for live audio rooms. - * - * Set to 1 to restore the original behaviour for testing or for - * deployments to relays that don't exhibit the stream-cliff bug. + * Set to a higher value to amortise per-stream overhead (CPU, + * relay bookkeeping) at the cost of late-join latency: with + * `framesPerGroup = 5` the late-join initial gap is ≤ 100 ms + * instead of ≤ 20 ms. */ private val framesPerGroup: Int = DEFAULT_FRAMES_PER_GROUP, ) { @@ -197,12 +198,10 @@ class NestMoqLiteBroadcaster( companion object { /** - * Default moq-lite group size = 5 Opus frames ≈ 100 ms of audio. - * Picked to keep the QUIC uni-stream creation rate under the - * production cliff observed against `nostrnests.com` while - * still giving late-joining subscribers a sub-100 ms initial - * audio gap. See [framesPerGroup] kdoc for the full rationale. + * Default moq-lite group size = 1 Opus frame per group, matching + * the JS reference broadcaster's wire shape. See [framesPerGroup] + * kdoc for the full rationale + history. */ - const val DEFAULT_FRAMES_PER_GROUP: Int = 5 + const val DEFAULT_FRAMES_PER_GROUP: Int = 1 } }