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.
This commit is contained in:
Andrew Steinborn
2020-05-24 10:56:26 -04:00
parent 742b8d98cb
commit b3bd773fea
19 changed files with 173 additions and 349 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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, () -> {