You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This PR cuts two per-packet allocations on the write path.
Void promises
BedrockPeer.flushPacketQueue called channel.write(packet), which allocates a ChannelPromise
per packet. The future is never used: the batch encoder completes it and drops it. The queue drain
now passes the channel's shared void promise instead.
Write failures still reach the pipeline as exceptions, since channel.voidPromise() returns the
pipeline's instance, which is constructed with fireException = true.
Length prefix written in place
Batching used to allocate a separate 5-byte buffer for each packet's VarInt length and add it as
its own component, so every packet cost one buffer allocation and two CompositeByteBuf.Components.
The packet codec now reserves 5 bytes (the maximum width of an unsigned 32-bit VarInt) ahead of the
packet by starting the encode buffer at offset 5:
At batch time the actual prefix width is known, so it is written right-aligned against the packet
and the reader index moves back by exactly that many bytes:
The component then covers [3, 205). The unused reserved bytes fall outside it and never reach the
wire, so the output is byte-identical to the old path and no padding is involved.
This removes one component per packet, plus the header buffer. In the profile below the components
(addComponent, 29.14%) cost about thirty times what the header buffers did (ioBuffer, 0.94%),
because the header buffer is pooled and recycled while every Component is a fresh object. The
component is where the saving is.
Notes
The encode buffer request is unchanged at 128 bytes with the reservation inside it, so it stays
on the same pooled size class.
Buffers that carry no reservation (pre-encoded, decoded, or caller-supplied) take the original
two-component path. reservedPrefixBytes defaults to 0 and setPacketBuffer clears it, so a
reservation cannot outlive the buffer it describes.
encodeHeader implementations are unaffected. _v1, _v2 and _v3 all write relatively
(writeByte, writeUnsignedInt), so a pre-positioned writer index is transparent to them.
Adds a test source set to common and tests pinning sizeOfUnsignedInt and setUnsignedInt to writeUnsignedInt, plus batch tests asserting both paths emit identical bytes.
Profile
This is a before profile, the measurement that drove these changes.
Yes, it is set to either 0 or 5 by the library itself. Now that you mentioned, I think we could change it to a byte instead, so we would save memory without making it hardcoded on 5 bytes prefix. I don't think making it a boolean and hardcoding to 5 bytes would buy us anything over using byte.
If we make it byte, then it will allow someone using the library to change how the prefixed bytes behaves depending on how they want it to work, maybe they can guarantee that specific packets are always smaller than some size and reduce the prefixed byte size.
Okay, after a quick check, the size of the BedrockPacketWrapper object is 48 bytes without int reservedPrefixBytes, adding any other field means that the object size will be aligned to Java's 8 byte alignment regardless if its a byte or int, so the object size will grow to 56 bytes. What we can do to keep BedrockPacketWrapper at 48 bytes is demoting the senderSubClientId and targetSubClientId types to byte instead, that would keep the BedrockPacketWrapper size at 48 bytes with the byte reservedPrefixBytes
I don't think senderSubClientId can be anything above 127 anyway so would it be okay to change?
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
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.
This PR cuts two per-packet allocations on the write path.
Void promises
BedrockPeer.flushPacketQueuecalledchannel.write(packet), which allocates aChannelPromiseper packet. The future is never used: the batch encoder completes it and drops it. The queue drain
now passes the channel's shared void promise instead.
Write failures still reach the pipeline as exceptions, since
channel.voidPromise()returns thepipeline's instance, which is constructed with
fireException = true.Length prefix written in place
Batching used to allocate a separate 5-byte buffer for each packet's VarInt length and add it as
its own component, so every packet cost one buffer allocation and two
CompositeByteBuf.Components.The packet codec now reserves 5 bytes (the maximum width of an unsigned 32-bit VarInt) ahead of the
packet by starting the encode buffer at offset 5:
At batch time the actual prefix width is known, so it is written right-aligned against the packet
and the reader index moves back by exactly that many bytes:
The component then covers
[3, 205). The unused reserved bytes fall outside it and never reach thewire, so the output is byte-identical to the old path and no padding is involved.
This removes one component per packet, plus the header buffer. In the profile below the components
(
addComponent, 29.14%) cost about thirty times what the header buffers did (ioBuffer, 0.94%),because the header buffer is pooled and recycled while every
Componentis a fresh object. Thecomponent is where the saving is.
Notes
on the same pooled size class.
two-component path.
reservedPrefixBytesdefaults to 0 andsetPacketBufferclears it, so areservation cannot outlive the buffer it describes.
encodeHeaderimplementations are unaffected._v1,_v2and_v3all write relatively(
writeByte,writeUnsignedInt), so a pre-positioned writer index is transparent to them.commonand tests pinningsizeOfUnsignedIntandsetUnsignedInttowriteUnsignedInt, plus batch tests asserting both paths emit identical bytes.Profile
This is a before profile, the measurement that drove these changes.