Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-16 21:10:30 +01:00
Do more speculative VarInt reading optimizations (#1418)
Dieser Commit ist enthalten in:
Ursprung
46f29480bd
Commit
784806848d
@ -104,6 +104,7 @@ public enum ProtocolUtils {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
||||||
|
private static final int MAXIMUM_VARINT_SIZE = 5;
|
||||||
private static final BinaryTagType<? extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] {
|
private static final BinaryTagType<? extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] {
|
||||||
BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT,
|
BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT,
|
||||||
BinaryTagTypes.LONG, BinaryTagTypes.FLOAT, BinaryTagTypes.DOUBLE,
|
BinaryTagTypes.LONG, BinaryTagTypes.FLOAT, BinaryTagTypes.DOUBLE,
|
||||||
@ -111,13 +112,18 @@ public enum ProtocolUtils {
|
|||||||
BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY};
|
BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY};
|
||||||
private static final QuietDecoderException BAD_VARINT_CACHED =
|
private static final QuietDecoderException BAD_VARINT_CACHED =
|
||||||
new QuietDecoderException("Bad VarInt decoded");
|
new QuietDecoderException("Bad VarInt decoded");
|
||||||
private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
|
private static final int[] VAR_INT_LENGTHS = new int[65];
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (int i = 0; i <= 32; ++i) {
|
for (int i = 0; i <= 32; ++i) {
|
||||||
VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
|
VAR_INT_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
|
||||||
}
|
}
|
||||||
VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0.
|
VAR_INT_LENGTHS[32] = 1; // Special case for the number 0.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecoderException badVarint() {
|
||||||
|
return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
|
||||||
|
: BAD_VARINT_CACHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,56 +133,29 @@ public enum ProtocolUtils {
|
|||||||
* @return the decoded VarInt
|
* @return the decoded VarInt
|
||||||
*/
|
*/
|
||||||
public static int readVarInt(ByteBuf buf) {
|
public static int readVarInt(ByteBuf buf) {
|
||||||
int read = readVarIntSafely(buf);
|
int readable = buf.readableBytes();
|
||||||
if (read == Integer.MIN_VALUE) {
|
if (readable == 0) {
|
||||||
throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
|
// special case for empty buffer
|
||||||
: BAD_VARINT_CACHED;
|
throw badVarint();
|
||||||
}
|
}
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// we can read at least one byte, and this should be a common case
|
||||||
* Reads a Minecraft-style VarInt from the specified {@code buf}. The difference between this
|
int k = buf.readByte();
|
||||||
* method and {@link #readVarInt(ByteBuf)} is that this function returns a sentinel value if the
|
if ((k & 0x80) != 128) {
|
||||||
* varint is invalid.
|
return k;
|
||||||
*
|
}
|
||||||
* @param buf the buffer to read from
|
|
||||||
* @return the decoded VarInt, or {@code Integer.MIN_VALUE} if the varint is invalid
|
// in case decoding one byte was not enough, use a loop to decode up to the next 4 bytes
|
||||||
*/
|
int maxRead = Math.min(MAXIMUM_VARINT_SIZE, readable);
|
||||||
public static int readVarIntSafely(ByteBuf buf) {
|
int i = k & 0x7F;
|
||||||
int i = 0;
|
for (int j = 1; j < maxRead; j++) {
|
||||||
int maxRead = Math.min(5, buf.readableBytes());
|
k = buf.readByte();
|
||||||
for (int j = 0; j < maxRead; j++) {
|
|
||||||
int k = buf.readByte();
|
|
||||||
i |= (k & 0x7F) << j * 7;
|
i |= (k & 0x7F) << j * 7;
|
||||||
if ((k & 0x80) != 128) {
|
if ((k & 0x80) != 128) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Integer.MIN_VALUE;
|
throw badVarint();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a Minecraft-style VarInt from the specified {@code buf}. The difference between this
|
|
||||||
* method and {@link #readVarInt(ByteBuf)} is that this function returns a sentinel value if the
|
|
||||||
* varint is invalid.
|
|
||||||
*
|
|
||||||
* @param buf the buffer to read from
|
|
||||||
* @return the decoded VarInt
|
|
||||||
* @throws DecoderException if the varint is invalid
|
|
||||||
*/
|
|
||||||
public static int readVarIntSafelyOrThrow(ByteBuf buf) {
|
|
||||||
int i = 0;
|
|
||||||
int maxRead = Math.min(5, buf.readableBytes());
|
|
||||||
for (int j = 0; j < maxRead; j++) {
|
|
||||||
int k = buf.readByte();
|
|
||||||
i |= (k & 0x7F) << j * 7;
|
|
||||||
if ((k & 0x80) != 128) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
|
|
||||||
: BAD_VARINT_CACHED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,7 +165,7 @@ public enum ProtocolUtils {
|
|||||||
* @return the byte size of {@code value} if encoded as a VarInt
|
* @return the byte size of {@code value} if encoded as a VarInt
|
||||||
*/
|
*/
|
||||||
public static int varIntBytes(int value) {
|
public static int varIntBytes(int value) {
|
||||||
return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -210,6 +189,8 @@ public enum ProtocolUtils {
|
|||||||
|
|
||||||
private static void writeVarIntFull(ByteBuf buf, int value) {
|
private static void writeVarIntFull(ByteBuf buf, int value) {
|
||||||
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||||
|
|
||||||
|
// This essentially is an unrolled version of the "traditional" VarInt encoding.
|
||||||
if ((value & (0xFFFFFFFF << 7)) == 0) {
|
if ((value & (0xFFFFFFFF << 7)) == 0) {
|
||||||
buf.writeByte(value);
|
buf.writeByte(value);
|
||||||
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
|
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.protocol.netty;
|
package com.velocitypowered.proxy.protocol.netty;
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.netty.VarintByteDecoder.DecodeResult;
|
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
|
||||||
|
|
||||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
@ -29,53 +30,114 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||||
|
|
||||||
private static final QuietDecoderException BAD_LENGTH_CACHED =
|
private static final QuietDecoderException BAD_PACKET_LENGTH =
|
||||||
new QuietDecoderException("Bad packet length");
|
new QuietDecoderException("Bad packet length");
|
||||||
private static final QuietDecoderException VARINT_BIG_CACHED =
|
private static final QuietDecoderException VARINT_TOO_BIG =
|
||||||
new QuietDecoderException("VarInt too big");
|
new QuietDecoderException("VarInt too big");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
|
||||||
|
throws Exception {
|
||||||
if (!ctx.channel().isActive()) {
|
if (!ctx.channel().isActive()) {
|
||||||
in.clear();
|
in.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final VarintByteDecoder reader = new VarintByteDecoder();
|
// skip any runs of 0x00 we might find
|
||||||
|
int packetStart = in.forEachByte(FIND_NON_NUL);
|
||||||
int varintEnd = in.forEachByte(reader);
|
if (packetStart == -1) {
|
||||||
if (varintEnd == -1) {
|
|
||||||
// We tried to go beyond the end of the buffer. This is probably a good sign that the
|
|
||||||
// buffer was too short to hold a proper varint.
|
|
||||||
if (reader.getResult() == DecodeResult.RUN_OF_ZEROES) {
|
|
||||||
// Special case where the entire packet is just a run of zeroes. We ignore them all.
|
|
||||||
in.clear();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
in.readerIndex(packetStart);
|
||||||
|
|
||||||
if (reader.getResult() == DecodeResult.RUN_OF_ZEROES) {
|
// try to read the length of the packet
|
||||||
// this will return to the point where the next varint starts
|
in.markReaderIndex();
|
||||||
in.readerIndex(varintEnd);
|
int preIndex = in.readerIndex();
|
||||||
} else if (reader.getResult() == DecodeResult.SUCCESS) {
|
int length = readRawVarInt21(in);
|
||||||
int readVarint = reader.getReadVarint();
|
if (preIndex == in.readerIndex()) {
|
||||||
int bytesRead = reader.getBytesRead();
|
return;
|
||||||
if (readVarint < 0) {
|
}
|
||||||
in.clear();
|
if (length < 0) {
|
||||||
throw BAD_LENGTH_CACHED;
|
throw BAD_PACKET_LENGTH;
|
||||||
} else if (readVarint == 0) {
|
}
|
||||||
// skip over the empty packet(s) and ignore it
|
|
||||||
in.readerIndex(varintEnd + 1);
|
// note that zero-length packets are ignored
|
||||||
|
if (length > 0) {
|
||||||
|
if (in.readableBytes() < length) {
|
||||||
|
in.resetReaderIndex();
|
||||||
} else {
|
} else {
|
||||||
int minimumRead = bytesRead + readVarint;
|
out.add(in.readRetainedSlice(length));
|
||||||
if (in.isReadable(minimumRead)) {
|
|
||||||
out.add(in.retainedSlice(varintEnd + 1, readVarint));
|
|
||||||
in.skipBytes(minimumRead);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (reader.getResult() == DecodeResult.TOO_BIG) {
|
|
||||||
in.clear();
|
|
||||||
throw VARINT_BIG_CACHED;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a VarInt from the buffer of up to 21 bits in size.
|
||||||
|
*
|
||||||
|
* @param buffer the buffer to read from
|
||||||
|
* @return the VarInt decoded, {@code 0} if no varint could be read
|
||||||
|
* @throws QuietDecoderException if the VarInt is too big to be decoded
|
||||||
|
*/
|
||||||
|
private static int readRawVarInt21(ByteBuf buffer) {
|
||||||
|
if (buffer.readableBytes() < 4) {
|
||||||
|
// we don't have enough that we can read a potentially full varint, so fall back to
|
||||||
|
// the slow path.
|
||||||
|
return readRawVarintSmallBuf(buffer);
|
||||||
|
}
|
||||||
|
int wholeOrMore = buffer.getIntLE(buffer.readerIndex());
|
||||||
|
|
||||||
|
// take the last three bytes and check if any of them have the high bit set
|
||||||
|
int atStop = ~wholeOrMore & 0x808080;
|
||||||
|
if (atStop == 0) {
|
||||||
|
// all bytes have the high bit set, so the varint we are trying to decode is too wide
|
||||||
|
throw VARINT_TOO_BIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1;
|
||||||
|
buffer.skipBytes(bitsToKeep >> 3);
|
||||||
|
|
||||||
|
// remove all bits we don't need to keep, a trick from
|
||||||
|
// https://github.com/netty/netty/pull/14050#issuecomment-2107750734:
|
||||||
|
//
|
||||||
|
// > The idea is that thisVarintMask has 0s above the first one of firstOneOnStop, and 1s at
|
||||||
|
// > and below it. For example if firstOneOnStop is 0x800080 (where the last 0x80 is the only
|
||||||
|
// > one that matters), then thisVarintMask is 0xFF.
|
||||||
|
//
|
||||||
|
// this is also documented in Hacker's Delight, section 2-1 "Manipulating Rightmost Bits"
|
||||||
|
int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1));
|
||||||
|
|
||||||
|
// merge together using this trick: https://github.com/netty/netty/pull/14050#discussion_r1597896639
|
||||||
|
preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1);
|
||||||
|
preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2);
|
||||||
|
return preservedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readRawVarintSmallBuf(ByteBuf buffer) {
|
||||||
|
if (!buffer.isReadable()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
buffer.markReaderIndex();
|
||||||
|
|
||||||
|
byte tmp = buffer.readByte();
|
||||||
|
if (tmp >= 0) {
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
int result = tmp & 0x7F;
|
||||||
|
if (!buffer.isReadable()) {
|
||||||
|
buffer.resetReaderIndex();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ((tmp = buffer.readByte()) >= 0) {
|
||||||
|
return result | tmp << 7;
|
||||||
|
}
|
||||||
|
result |= (tmp & 0x7F) << 7;
|
||||||
|
if (!buffer.isReadable()) {
|
||||||
|
buffer.resetReaderIndex();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ((tmp = buffer.readByte()) >= 0) {
|
||||||
|
return result | tmp << 14;
|
||||||
|
}
|
||||||
|
return result | (tmp & 0x7F) << 14;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020-2021 Velocity Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.velocitypowered.proxy.protocol.netty;
|
|
||||||
|
|
||||||
import io.netty.util.ByteProcessor;
|
|
||||||
|
|
||||||
class VarintByteDecoder implements ByteProcessor {
|
|
||||||
|
|
||||||
private int readVarint;
|
|
||||||
private int bytesRead;
|
|
||||||
private DecodeResult result = DecodeResult.TOO_SHORT;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean process(byte k) {
|
|
||||||
if (k == 0 && bytesRead == 0) {
|
|
||||||
// tentatively say it's invalid, but there's a possibility of redemption
|
|
||||||
result = DecodeResult.RUN_OF_ZEROES;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (result == DecodeResult.RUN_OF_ZEROES) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
readVarint |= (k & 0x7F) << bytesRead++ * 7;
|
|
||||||
if (bytesRead > 3) {
|
|
||||||
result = DecodeResult.TOO_BIG;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ((k & 0x80) != 128) {
|
|
||||||
result = DecodeResult.SUCCESS;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getReadVarint() {
|
|
||||||
return readVarint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBytesRead() {
|
|
||||||
return bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DecodeResult getResult() {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DecodeResult {
|
|
||||||
SUCCESS,
|
|
||||||
TOO_SHORT,
|
|
||||||
TOO_BIG,
|
|
||||||
RUN_OF_ZEROES
|
|
||||||
}
|
|
||||||
}
|
|
@ -63,7 +63,7 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||||
this.id = ProtocolUtils.readVarIntSafelyOrThrow(buf);
|
this.id = ProtocolUtils.readVarInt(buf);
|
||||||
this.channel = ProtocolUtils.readString(buf);
|
this.channel = ProtocolUtils.readString(buf);
|
||||||
if (buf.isReadable()) {
|
if (buf.isReadable()) {
|
||||||
this.replace(buf.readRetainedSlice(buf.readableBytes()));
|
this.replace(buf.readRetainedSlice(buf.readableBytes()));
|
||||||
|
@ -70,7 +70,7 @@ public class ProtocolUtilsTest {
|
|||||||
private void writeReadTestOld(ByteBuf buf, int test) {
|
private void writeReadTestOld(ByteBuf buf, int test) {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
writeVarIntOld(buf, test);
|
writeVarIntOld(buf, test);
|
||||||
assertEquals(test, ProtocolUtils.readVarIntSafely(buf));
|
assertEquals(test, ProtocolUtils.readVarInt(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -103,7 +103,7 @@ public class ProtocolUtilsTest {
|
|||||||
"Encoding of " + i + " was invalid");
|
"Encoding of " + i + " was invalid");
|
||||||
|
|
||||||
assertEquals(i, oldReadVarIntSafely(varintNew));
|
assertEquals(i, oldReadVarIntSafely(varintNew));
|
||||||
assertEquals(i, ProtocolUtils.readVarIntSafely(varintOld));
|
assertEquals(i, ProtocolUtils.readVarInt(varintOld));
|
||||||
|
|
||||||
varintNew.clear();
|
varintNew.clear();
|
||||||
varintOld.clear();
|
varintOld.clear();
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren