Remove up to 50 ms of outbound packet latency - #341
Merged
Conversation
Contributor
|
Nice, I implemented this yesterday oomph-ac/oomph#147 |
SendableMetatype
force-pushed
the
on-demand-flush
branch
from
July 25, 2026 18:06
6f11eef to
9be14de
Compare
The fixed 50ms tick dates back to the 2019 common refactor and assumes the packet producer runs a 20 TPS game loop, which holds for a game server but not for a proxy translating a remote server's traffic, where it added 0 to 50ms to every outbound batch. Enqueueing now schedules a single flush 1ms out, so a tick burst still coalesces into one batch and compression and framing behavior are unchanged, while the tick latency is gone. Reliability is untouched; RakNet keeps its own session tick. The closing state keeps its existing semantics: once a disconnect is requested nothing more is written, and onClose() frees whatever remains queued.
The closing and closed door gates on the send paths are check then act: a producer can pass the gate just as teardown begins, enqueueing a wrapper that nothing would ever release since the queue stops being drained. The closed branches of flushPacketQueue() and schedulePacketFlush() now drain it, handed to the event loop since the MPSC queue permits one consumer, with free() synchronized because a rejected execute means the loop is shut down or saturated and may still be running tasks. A rejected flush schedule no longer rethrows: the wrapper stays owned by the queue, and a deduplicated retry on GlobalEventExecutor, which never rejects, re-enters the scheduling path until the loop accepts or the peer closes, so a live peer with no further sends cannot strand a packet and saturation cannot fan out into an unbounded global executor backlog. onClose() iterates a snapshot of the session map, since sessions remove themselves during teardown and mutating the open addressing map mid iteration corrupts it (reproducible with ids 0, 1, 33, 66, reachable from the wire via inbound target client ids); the drain now sits in a finally so a throwing session can no longer skip it forever. An off loop close() retries off the global executor when the loop rejects it, so the disconnect reason cannot be silently lost while the channel stays open.
The 1 ms coalescing window costs nothing for producers that hand a burst over together, but one that emits a burst over a longer stretch splits it into batches that each compress on their own. Deployments that would rather trade latency for compression efficiency can set the window with the org.cloudburstmc.protocol.bedrock.batchFlushDelayMillis system property, alongside the existing disconnectTimeout property. The default stays at 1 ms and values outside 0 to 50 fall back to it.
SendableMetatype
force-pushed
the
on-demand-flush
branch
from
August 6, 2026 21:42
9be14de to
42e0115
Compare
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.
Outbound packets wait for a fixed 50 ms flush tick, which adds 0 to 50 ms to every batch when the packet producer is not a local game loop (proxies, standalone tools). Packets are now flushed 1 ms after enqueueing, so the wait is bounded by ~1 ms plus timer granularity, while batching, compression, and reliability behavior stay as they are.
Notes for review:
RAK_AUTO_FLUSHon (the default), the RakNet layer ignores explicit flushes: datagram cadence stays on the existing 10 ms session tick, and the worst case wait still drops from ~60 ms to ~10 ms. SettingRAK_AUTO_FLUSH=falsegives the full on demand path.sendPacketImmediately()is unchanged.RAK_AUTO_FLUSHor override the affected code.Results: in my own testing on Velocity with Geyser (
RAK_AUTO_FLUSH=false), backend measured player ping dropped from 50-100 ms to 20-45 ms. Combined with separate Geyser side changes that make its ping measurement more accurate, essentially every user who tested the release reported noticeably better ping.