13
0
geforkt von Mirrors/Velocity

Switch out Cloudflare zlib for libdeflate.

libdeflate is significantly faster than vanilla zlib, zlib-ng, and Cloudflare zlib. It is also MIT-licensed (so no licensing concerns). In addition, it simplifies a lot of the native code (something that's been tricky to get right).

While we're at it, I have also taken the time to fine-time compression in Velocity in general. Thanks to this work, native compression only requires one JNI call, an improvement from the more than 2 (sometimes up to 5) that were possible before. This optimization also extends to the existing Java compressors, though they require potentially two JNI calls.
Dieser Commit ist enthalten in:
Andrew Steinborn 2020-05-24 10:56:26 -04:00
Ursprung 742b8d98cb
Commit b3bd773fea
19 geänderte Dateien mit 173 neuen und 349 gelöschten Zeilen

1
.gitignore vendored
Datei anzeigen

@ -90,3 +90,4 @@ plugins/
native/mbedtls
native/zlib-ng
native/zlib-cf
native/libdeflate

Datei anzeigen

@ -1,21 +0,0 @@
#!/bin/bash
if [ ! -d zlib-cf ]; then
echo "Cloning Cloudflare zlib..."
git clone -b gcc.amd64 https://github.com/cloudflare/zlib.git zlib-cf
fi
echo "Compiling Cloudflare zlib..."
cd zlib-cf
CFLAGS="-fPIC -O3 -flto" AR=gcc-ar RANLIB=gcc-ranlib ./configure --static
make clean && make
cd ..
# Modify as you need.
MBEDTLS_ROOT=mbedtls
CFLAGS="-O3 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack"
gcc $CFLAGS -Izlib-cf src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \
src/main/c/jni_zlib_common.c zlib-cf/libz.a -Wl,-z,noexecstack -o src/main/resources/linux_x64/velocity-compress.so
gcc $CFLAGS -I $MBEDTLS_ROOT/include -shared $MBEDTLS_ROOT/library/aes.c $MBEDTLS_ROOT/library/aesni.c \
$MBEDTLS_ROOT/library/platform.c $MBEDTLS_ROOT/library/platform_util.c src/main/c/jni_util.c src/main/c/jni_cipher.c \
-o src/main/resources/linux_x64/velocity-cipher.so

Datei anzeigen

@ -1,21 +1,20 @@
#!/bin/bash
if [ ! -d zlib-ng ]; then
echo "Cloning zlib-ng..."
git clone https://github.com/zlib-ng/zlib-ng.git
if [ ! -d libdeflate ]; then
echo "Cloning libdeflate..."
git clone https://github.com/ebiggers/libdeflate.git
fi
echo "Compiling zlib-ng..."
cd zlib-ng
CFLAGS="-fPIC -O3" ./configure --zlib-compat --static
make clean && make
echo "Compiling libdeflate..."
cd libdeflate || exit
make
cd ..
# Modify as you need.
MBEDTLS_ROOT=mbedtls
CFLAGS="-O3 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared"
gcc $CFLAGS -Izlib-ng src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \
src/main/c/jni_zlib_common.c zlib-ng/libz.a -o src/main/resources/linux_x64/velocity-compress.so
gcc $CFLAGS -I $MBEDTLS_ROOT/include -shared $MBEDTLS_ROOT/library/aes.c $MBEDTLS_ROOT/library/aesni.c \
CFLAGS="-O3 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack"
gcc $CFLAGS -Ilibdeflate src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \
libdeflate/libdeflate.a -o src/main/resources/linux_x64/velocity-compress.so
gcc $CFLAGS -I $MBEDTLS_ROOT/include -shared $MBEDTLS_ROOT/library/aes.c $MBEDTLS_ROOT/library/aesni.c \
$MBEDTLS_ROOT/library/platform.c $MBEDTLS_ROOT/library/platform_util.c src/main/c/jni_util.c src/main/c/jni_cipher.c \
-o src/main/resources/linux_x64/velocity-cipher.so
-o src/main/resources/linux_x64/velocity-cipher.so

Datei anzeigen

@ -1,29 +0,0 @@
#include <jni.h>
#include <stdbool.h>
#include <stdlib.h>
#include <zlib.h>
#include "jni_util.h"
void JNICALL
check_zlib_free(JNIEnv *env, z_stream *stream, bool deflate)
{
int ret = deflate ? deflateEnd(stream) : inflateEnd(stream);
const char *msg = stream->msg;
free((void*) stream);
switch (ret) {
case Z_OK:
break;
case Z_STREAM_ERROR:
if (msg == NULL) {
msg = "stream state inconsistent";
}
// fall-through
case Z_DATA_ERROR:
if (msg == NULL) {
msg = "data was discarded";
}
throwException(env, "java/lang/IllegalArgumentException", msg);
break;
}
}

Datei anzeigen

@ -1,6 +0,0 @@
#include <jni.h>
#include <stdbool.h>
#include <zlib.h>
void JNICALL
check_zlib_free(JNIEnv *env, z_stream *stream, bool deflate);

Datei anzeigen

@ -2,56 +2,21 @@
#include <jni.h>
#include <stdbool.h>
#include <stdlib.h>
#include <zlib.h>
#include <libdeflate.h>
#include "jni_util.h"
#include "jni_zlib_common.h"
static jfieldID finishedID;
static jfieldID consumedID;
JNIEXPORT void JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibDeflate_initIDs(JNIEnv *env, jclass cls)
{
finishedID = (*env)->GetFieldID(env, cls, "finished", "Z");
consumedID = (*env)->GetFieldID(env, cls, "consumed", "I");
}
JNIEXPORT jlong JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibDeflate_init(JNIEnv *env,
jobject obj,
jint level)
{
z_stream* stream = calloc(1, sizeof(z_stream));
if (stream == 0) {
struct libdeflate_compressor *compressor = libdeflate_alloc_compressor(level);
if (compressor == NULL) {
// Out of memory!
throwException(env, "java/lang/OutOfMemoryError", "zlib allocate stream");
return 0;
}
int ret = deflateInit(stream, level);
if (ret == Z_OK) {
return (jlong) stream;
} else {
const char *zlib_msg = stream->msg;
free(stream);
switch (ret) {
case Z_MEM_ERROR:
throwException(env, "java/lang/OutOfMemoryError", "zlib init");
break;
case Z_STREAM_ERROR: {
// Thanks Ken and Ritchie!
char message[32];
snprintf(message, 32, "invalid level %d", level);
throwException(env, "java/lang/IllegalArgumentException", message);
break;
}
default:
throwException(env, "java/util/zip/DataFormatException", zlib_msg);
break;
}
throwException(env, "java/lang/OutOfMemoryError", "libdeflate allocate compressor");
return 0;
}
return (jlong) compressor;
}
JNIEXPORT void JNICALL
@ -59,11 +24,10 @@ Java_com_velocitypowered_natives_compression_NativeZlibDeflate_free(JNIEnv *env,
jobject obj,
jlong ctx)
{
z_stream* stream = (z_stream*) ctx;
check_zlib_free(env, stream, true);
libdeflate_free_compressor((struct libdeflate_compressor *) ctx);
}
JNIEXPORT int JNICALL
JNIEXPORT jboolean JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibDeflate_process(JNIEnv *env,
jobject obj,
jlong ctx,
@ -73,38 +37,8 @@ Java_com_velocitypowered_natives_compression_NativeZlibDeflate_process(JNIEnv *e
jint destinationLength,
jboolean finish)
{
z_stream* stream = (z_stream*) ctx;
stream->next_in = (Bytef *) sourceAddress;
stream->next_out = (Bytef *) destinationAddress;
stream->avail_in = sourceLength;
stream->avail_out = destinationLength;
int res = deflate(stream, finish ? Z_FINISH : Z_NO_FLUSH);
switch (res) {
case Z_STREAM_END:
// The stream has ended.
(*env)->SetBooleanField(env, obj, finishedID, JNI_TRUE);
// fall-through
case Z_OK:
// Not yet completed, but progress has been made. Tell Java how many bytes we've processed.
(*env)->SetIntField(env, obj, consumedID, sourceLength - stream->avail_in);
return destinationLength - stream->avail_out;
case Z_BUF_ERROR:
// This is not fatal. Just say we need more data. Usually this applies to the next_out buffer,
// which NativeVelocityCompressor will notice and will expand the buffer.
return 0;
default:
throwException(env, "java/util/zip/DataFormatException", stream->msg);
return 0;
}
}
JNIEXPORT void JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibDeflate_reset(JNIEnv *env,
jobject obj,
jlong ctx)
{
z_stream* stream = (z_stream*) ctx;
int ret = deflateReset(stream);
assert(ret == Z_OK);
struct libdeflate_compressor *compressor = (struct libdeflate_compressor *) ctx;
size_t produced = libdeflate_zlib_compress(compressor, (void *) sourceAddress, sourceLength,
(void *) destinationAddress, destinationLength);
return (jlong) produced;
}

Datei anzeigen

@ -2,50 +2,21 @@
#include <jni.h>
#include <stdbool.h>
#include <stdlib.h>
#include <zlib.h>
#include <libdeflate.h>
#include "jni_util.h"
#include "jni_zlib_common.h"
static jfieldID finishedID;
static jfieldID consumedID;
JNIEXPORT void JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibInflate_initIDs(JNIEnv *env, jclass cls)
{
finishedID = (*env)->GetFieldID(env, cls, "finished", "Z");
consumedID = (*env)->GetFieldID(env, cls, "consumed", "I");
}
JNIEXPORT jlong JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibInflate_init(JNIEnv *env,
jobject obj)
{
z_stream* stream = calloc(1, sizeof(z_stream));
if (stream == 0) {
struct libdeflate_decompressor *decompress = libdeflate_alloc_decompressor();
if (decompress == NULL) {
// Out of memory!
throwException(env, "java/lang/OutOfMemoryError", "zlib allocate stream");
throwException(env, "java/lang/OutOfMemoryError", "libdeflate allocate decompressor");
return 0;
}
int ret = inflateInit(stream);
if (ret == Z_OK) {
return (jlong) stream;
} else {
const char *zlib_msg = stream->msg;
free(stream);
switch (ret) {
case Z_MEM_ERROR:
throwException(env, "java/lang/OutOfMemoryError", "zlib init");
return 0;
case Z_STREAM_ERROR:
throwException(env, "java/lang/IllegalArgumentException", "stream clobbered?");
return 0;
default:
throwException(env, "java/util/zip/DataFormatException", zlib_msg);
return 0;
}
}
return (jlong) decompress;
}
JNIEXPORT void JNICALL
@ -53,51 +24,34 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_free(JNIEnv *env,
jobject obj,
jlong ctx)
{
z_stream* stream = (z_stream*) ctx;
check_zlib_free(env, stream, false);
libdeflate_free_decompressor((struct libdeflate_decompressor *) ctx);
}
JNIEXPORT int JNICALL
JNIEXPORT jboolean JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *env,
jobject obj,
jlong ctx,
jlong sourceAddress,
jint sourceLength,
jlong destinationAddress,
jint destinationLength)
jint destinationLength,
jlong maximumSize)
{
z_stream* stream = (z_stream*) ctx;
stream->next_in = (Bytef *) sourceAddress;
stream->next_out = (Bytef *) destinationAddress;
stream->avail_in = sourceLength;
stream->avail_out = destinationLength;
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
sourceLength, (void *) destinationAddress, destinationLength, NULL);
int res = inflate(stream, Z_PARTIAL_FLUSH);
switch (res) {
case Z_STREAM_END:
// The stream has ended
(*env)->SetBooleanField(env, obj, finishedID, JNI_TRUE);
// fall-through
case Z_OK:
// Not yet completed, but progress has been made. Tell Java how many bytes we've processed.
(*env)->SetIntField(env, obj, consumedID, sourceLength - stream->avail_in);
return destinationLength - stream->avail_out;
case Z_BUF_ERROR:
// This is not fatal. Just say we need more data. Usually this applies to the next_out buffer,
// which NativeVelocityCompressor will notice and will expand the buffer.
return 0;
default:
throwException(env, "java/util/zip/DataFormatException", stream->msg);
return 0;
switch (result) {
case LIBDEFLATE_SUCCESS:
// We are happy
return JNI_TRUE;
case LIBDEFLATE_BAD_DATA:
throwException(env, "java/util/zip/DataFormatException", "inflate data is bad");
return JNI_FALSE;
case LIBDEFLATE_SHORT_OUTPUT:
case LIBDEFLATE_INSUFFICIENT_SPACE:
// These cases are the same for us. We expect the full uncompressed size to be known.
throwException(env, "java/util/zip/DataFormatException", "uncompressed size is inaccurate");
return JNI_FALSE;
}
}
JNIEXPORT void JNICALL
Java_com_velocitypowered_natives_compression_NativeZlibInflate_reset(JNIEnv *env,
jobject obj,
jlong ctx)
{
z_stream* stream = (z_stream*) ctx;
int ret = inflateReset(stream);
assert(ret == Z_OK);
}

Datei anzeigen

@ -54,7 +54,8 @@ public class Java11VelocityCompressor implements VelocityCompressor {
}
@Override
public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException {
public void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize)
throws DataFormatException {
ensureNotDisposed();
// We (probably) can't nicely deal with >=1 buffer nicely, so let's scream loudly.
@ -67,7 +68,7 @@ public class Java11VelocityCompressor implements VelocityCompressor {
while (!inflater.finished() && inflater.getBytesRead() < source.readableBytes()) {
if (!destination.isWritable()) {
ensureMaxSize(destination, max);
ensureMaxSize(destination, uncompressedSize);
destination.ensureWritable(ZLIB_BUFFER_SIZE);
}

Datei anzeigen

@ -25,20 +25,21 @@ public class JavaVelocityCompressor implements VelocityCompressor {
}
@Override
public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException {
public void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize)
throws DataFormatException {
ensureNotDisposed();
final int available = source.readableBytes();
this.setInflaterInput(source);
if (destination.hasArray()) {
this.inflateDestinationIsHeap(destination, available, max);
this.inflateDestinationIsHeap(destination, available, uncompressedSize);
} else {
if (buf.length == 0) {
buf = new byte[ZLIB_BUFFER_SIZE];
}
while (!inflater.finished() && inflater.getBytesRead() < available) {
ensureMaxSize(destination, max);
ensureMaxSize(destination, uncompressedSize);
int read = inflater.inflate(buf);
destination.writeBytes(buf, 0, read);
}

Datei anzeigen

@ -0,0 +1,87 @@
package com.velocitypowered.natives.compression;
import com.google.common.base.Preconditions;
import com.velocitypowered.natives.util.BufferPreference;
import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;
public class LibdeflateVelocityCompressor implements VelocityCompressor {
public static final VelocityCompressorFactory FACTORY = LibdeflateVelocityCompressor::new;
private final NativeZlibInflate inflate = new NativeZlibInflate();
private final long inflateCtx;
private final NativeZlibDeflate deflate = new NativeZlibDeflate();
private final long deflateCtx;
private boolean disposed = false;
private LibdeflateVelocityCompressor(int level) {
int correctedLevel = level == -1 ? 6 : level;
if (correctedLevel > 12 || correctedLevel < 1) {
throw new IllegalArgumentException("Invalid compression level " + level);
}
this.inflateCtx = inflate.init();
this.deflateCtx = deflate.init(level == -1 ? 6 : level);
}
@Override
public void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize)
throws DataFormatException {
ensureNotDisposed();
source.memoryAddress();
destination.memoryAddress();
// libdeflate recommends we work with a known uncompressed size - so we work strictly within
// those parameters. If the uncompressed size doesn't match the compressed size, then we will
// throw an exception from native code.
destination.ensureWritable(uncompressedSize);
long sourceAddress = source.memoryAddress() + source.readerIndex();
long destinationAddress = destination.memoryAddress() + destination.writerIndex();
inflate.process(inflateCtx, sourceAddress, source.readableBytes(), destinationAddress,
uncompressedSize);
destination.writerIndex(destination.writerIndex() + uncompressedSize);
}
@Override
public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException {
ensureNotDisposed();
source.memoryAddress();
destination.memoryAddress();
while (true) {
long sourceAddress = source.memoryAddress() + source.readerIndex();
long destinationAddress = destination.memoryAddress() + destination.writerIndex();
int produced = deflate.process(deflateCtx, sourceAddress, source.readableBytes(),
destinationAddress, destination.writableBytes());
if (produced > 0) {
destination.writerIndex(destination.writerIndex() + produced);
return;
}
// Insufficient room - enlarge the buffer.
destination.capacity(destination.capacity() * 2);
}
}
private void ensureNotDisposed() {
Preconditions.checkState(!disposed, "Object already disposed");
}
@Override
public void dispose() {
if (!disposed) {
inflate.free(inflateCtx);
deflate.free(deflateCtx);
}
disposed = true;
}
@Override
public BufferPreference preferredBufferType() {
return BufferPreference.DIRECT_REQUIRED;
}
}

Datei anzeigen

@ -1,89 +0,0 @@
package com.velocitypowered.natives.compression;
import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFER_SIZE;
import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize;
import com.google.common.base.Preconditions;
import com.velocitypowered.natives.util.BufferPreference;
import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;
public class NativeVelocityCompressor implements VelocityCompressor {
public static final VelocityCompressorFactory FACTORY = NativeVelocityCompressor::new;
private final NativeZlibInflate inflate = new NativeZlibInflate();
private final long inflateCtx;
private final NativeZlibDeflate deflate = new NativeZlibDeflate();
private final long deflateCtx;
private boolean disposed = false;
private NativeVelocityCompressor(int level) {
this.inflateCtx = inflate.init();
this.deflateCtx = deflate.init(level);
}
@Override
public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException {
ensureNotDisposed();
source.memoryAddress();
destination.memoryAddress();
while (!inflate.finished && source.isReadable()) {
if (!destination.isWritable()) {
ensureMaxSize(destination, max);
destination.ensureWritable(ZLIB_BUFFER_SIZE);
}
int produced = inflate.process(inflateCtx, source.memoryAddress() + source.readerIndex(),
source.readableBytes(), destination.memoryAddress() + destination.writerIndex(),
destination.writableBytes());
source.readerIndex(source.readerIndex() + inflate.consumed);
destination.writerIndex(destination.writerIndex() + produced);
}
inflate.reset(inflateCtx);
inflate.consumed = 0;
inflate.finished = false;
}
@Override
public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException {
ensureNotDisposed();
source.memoryAddress();
destination.memoryAddress();
while (!deflate.finished) {
if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE);
}
int produced = deflate.process(deflateCtx, source.memoryAddress() + source.readerIndex(),
source.readableBytes(),
destination.memoryAddress() + destination.writerIndex(), destination.writableBytes(),
true);
source.readerIndex(source.readerIndex() + deflate.consumed);
destination.writerIndex(destination.writerIndex() + produced);
}
deflate.reset(deflateCtx);
deflate.consumed = 0;
deflate.finished = false;
}
private void ensureNotDisposed() {
Preconditions.checkState(!disposed, "Object already disposed");
}
@Override
public void dispose() {
if (!disposed) {
inflate.free(inflateCtx);
deflate.free(deflateCtx);
}
disposed = true;
}
@Override
public BufferPreference preferredBufferType() {
return BufferPreference.DIRECT_REQUIRED;
}
}

Datei anzeigen

@ -5,21 +5,10 @@ package com.velocitypowered.natives.compression;
*/
class NativeZlibDeflate {
boolean finished;
int consumed;
native long init(int level);
native long free(long ctx);
native int process(long ctx, long sourceAddress, int sourceLength, long destinationAddress,
int destinationLength, boolean finish);
native void reset(long ctx);
static {
initIDs();
}
private static native void initIDs();
int destinationLength);
}

Datei anzeigen

@ -1,25 +1,16 @@
package com.velocitypowered.natives.compression;
import java.util.zip.DataFormatException;
/**
* Represents a native interface for zlib's inflate functions.
*/
class NativeZlibInflate {
boolean finished;
int consumed;
native long init();
native long free(long ctx);
native int process(long ctx, long sourceAddress, int sourceLength, long destinationAddress,
int destinationLength);
native void reset(long ctx);
static {
initIDs();
}
private static native void initIDs();
native boolean process(long ctx, long sourceAddress, int sourceLength, long destinationAddress,
int destinationLength) throws DataFormatException;
}

Datei anzeigen

@ -6,10 +6,12 @@ import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;
/**
* Provides an interface to inflate and deflate {@link ByteBuf}s using zlib.
* Provides an interface to inflate and deflate {@link ByteBuf}s using zlib or a compatible
* implementation.
*/
public interface VelocityCompressor extends Disposable, Native {
void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException;
void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize)
throws DataFormatException;
void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException;
}

Datei anzeigen

@ -4,7 +4,7 @@ import com.google.common.collect.ImmutableList;
import com.velocitypowered.natives.NativeSetupException;
import com.velocitypowered.natives.compression.Java11VelocityCompressor;
import com.velocitypowered.natives.compression.JavaVelocityCompressor;
import com.velocitypowered.natives.compression.NativeVelocityCompressor;
import com.velocitypowered.natives.compression.LibdeflateVelocityCompressor;
import com.velocitypowered.natives.compression.VelocityCompressorFactory;
import com.velocitypowered.natives.encryption.JavaVelocityCipher;
import com.velocitypowered.natives.encryption.NativeVelocityCipher;
@ -64,10 +64,11 @@ public class Natives {
ImmutableList.of(
new NativeCodeLoader.Variant<>(NativeConstraints.MACOS,
copyAndLoadNative("/macosx/velocity-compress.dylib"), "native (macOS)",
NativeVelocityCompressor.FACTORY),
LibdeflateVelocityCompressor.FACTORY),
new NativeCodeLoader.Variant<>(NativeConstraints.LINUX,
copyAndLoadNative("/linux_x64/velocity-compress.so"), "native (Linux amd64)",
NativeVelocityCompressor.FACTORY),
copyAndLoadNative("/linux_x64/velocity-compress.so"),
"libdeflate (Linux amd64)",
LibdeflateVelocityCompressor.FACTORY),
new NativeCodeLoader.Variant<>(NativeConstraints.JAVA_11, () -> {
}, "Java 11", () -> Java11VelocityCompressor.FACTORY),
new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> {

Datei anzeigen

@ -86,10 +86,11 @@ class VelocityCompressorTest {
ByteBuf decompressed = bufSupplier.get();
source.writeBytes(TEST_DATA);
int uncompressedData = source.readableBytes();
try {
compressor.deflate(source, dest);
compressor.inflate(dest, decompressed, Integer.MAX_VALUE);
compressor.inflate(dest, decompressed, uncompressedData);
source.readerIndex(0);
assertTrue(ByteBufUtil.equals(source, decompressed));
} finally {

Datei anzeigen

@ -13,9 +13,13 @@ import java.util.List;
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int SOFT_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
private static final int UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
private final int threshold;
private final VelocityCompressor compressor;
@ -28,20 +32,21 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int claimedUncompressedSize = ProtocolUtils.readVarInt(in);
if (claimedUncompressedSize == 0) {
// Strip the now-useless uncompressed size, this message is already uncompressed.
// This message is not compressed.
out.add(in.retainedSlice());
return;
}
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold);
int allowedMax = Math.min(claimedUncompressedSize, HARD_MAXIMUM_UNCOMPRESSED_SIZE);
int initialCapacity = Math.min(claimedUncompressedSize, SOFT_MAXIMUM_UNCOMPRESSED_SIZE);
checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
UNCOMPRESSED_CAP);
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try {
compressor.inflate(compatibleIn, uncompressed, allowedMax);
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
out.add(uncompressed);
} catch (Exception e) {
uncompressed.release();

Datei anzeigen

@ -38,8 +38,11 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
throws Exception {
int initialBufferSize = msg.readableBytes() <= threshold ? msg.readableBytes() + 1 :
msg.readableBytes() / 3;
// Follow the advice of https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103
// here for compression. The maximum buffer size if the data compresses well (which is almost
// always the case) is one less the input buffer.
int offset = msg.readableBytes() < threshold ? 1 : -1;
int initialBufferSize = msg.readableBytes() + offset;
return MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
}