diff --git a/native/src/main/java/com/velocitypowered/natives/Native.java b/native/src/main/java/com/velocitypowered/natives/Native.java index 26cdd0ba4..e90ccf80a 100644 --- a/native/src/main/java/com/velocitypowered/natives/Native.java +++ b/native/src/main/java/com/velocitypowered/natives/Native.java @@ -1,5 +1,7 @@ package com.velocitypowered.natives; +import com.velocitypowered.natives.util.BufferPreference; + public interface Native { - boolean isNative(); + BufferPreference preferredBufferType(); } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java new file mode 100644 index 000000000..034126bc8 --- /dev/null +++ b/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java @@ -0,0 +1,133 @@ +package com.velocitypowered.natives.compression; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFER_SIZE; +import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize; + +import com.velocitypowered.natives.util.BufferPreference; +import io.netty.buffer.ByteBuf; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public class Java11VelocityCompressor implements VelocityCompressor { + + public static final VelocityCompressorFactory FACTORY = Java11VelocityCompressor::new; + + // The use of MethodHandle is intentional. Velocity targets Java 8, and these methods don't exist + // in Java 8. This was also the most performant solution I could find, only slightly slower than a + // direct method call without long warmup times, requiring bytecode generation through ASM, or + // other stuff. + private static final MethodHandle DEFLATE_SET_INPUT; + private static final MethodHandle INFLATE_SET_INPUT; + private static final MethodHandle DEFLATE_CALL; + private static final MethodHandle INFLATE_CALL; + + static { + try { + DEFLATE_SET_INPUT = MethodHandles.lookup().findVirtual(Deflater.class, "setInput", + MethodType.methodType(void.class, ByteBuffer.class)); + INFLATE_SET_INPUT = MethodHandles.lookup().findVirtual(Inflater.class, "setInput", + MethodType.methodType(void.class, ByteBuffer.class)); + + DEFLATE_CALL = MethodHandles.lookup().findVirtual(Deflater.class, "deflate", + MethodType.methodType(int.class, ByteBuffer.class)); + INFLATE_CALL = MethodHandles.lookup().findVirtual(Inflater.class, "inflate", + MethodType.methodType(int.class, ByteBuffer.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError("Can't use Java 11 compressor on your version of Java"); + } + } + + private final Deflater deflater; + private final Inflater inflater; + private boolean disposed = false; + + private Java11VelocityCompressor(int level) { + this.deflater = new Deflater(level); + this.inflater = new Inflater(); + } + + @Override + public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException { + ensureNotDisposed(); + + // We (probably) can't nicely deal with >=1 buffer nicely, so let's scream loudly. + checkArgument(source.nioBufferCount() == 1, "source has multiple backing buffers"); + checkArgument(destination.nioBufferCount() == 1, "destination has multiple backing buffers"); + + try { + int origIdx = source.readerIndex(); + INFLATE_SET_INPUT.invokeExact(inflater, source.nioBuffer()); + + while (!inflater.finished() && inflater.getBytesRead() < source.readableBytes()) { + if (!destination.isWritable()) { + ensureMaxSize(destination, max); + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + + ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), + destination.writableBytes()); + int produced = (int) INFLATE_CALL.invokeExact(inflater, destNioBuf); + source.readerIndex(origIdx + inflater.getTotalIn()); + destination.writerIndex(destination.writerIndex() + produced); + } + + inflater.reset(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException { + ensureNotDisposed(); + + // We (probably) can't nicely deal with >=1 buffer nicely, so let's scream loudly. + checkArgument(source.nioBufferCount() == 1, "source has multiple backing buffers"); + checkArgument(destination.nioBufferCount() == 1, "destination has multiple backing buffers"); + + try { + int origIdx = source.readerIndex(); + DEFLATE_SET_INPUT.invokeExact(deflater, source.nioBuffer()); + deflater.finish(); + + while (!deflater.finished()) { + if (!destination.isWritable()) { + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + + ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), + destination.writableBytes()); + int produced = (int) DEFLATE_CALL.invokeExact(deflater, destNioBuf); + source.readerIndex(origIdx + deflater.getTotalIn()); + destination.writerIndex(destination.writerIndex() + produced); + } + + deflater.reset(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public void dispose() { + disposed = true; + deflater.end(); + inflater.end(); + } + + private void ensureNotDisposed() { + checkState(!disposed, "Object already disposed"); + } + + @Override + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_PREFERRED; + } +} diff --git a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java index 633fc4f1d..9472702aa 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java @@ -4,6 +4,7 @@ import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFE 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; import java.util.zip.Deflater; @@ -77,7 +78,7 @@ public class JavaVelocityCompressor implements VelocityCompressor { } @Override - public boolean isNative() { - return false; + public BufferPreference preferredBufferType() { + return BufferPreference.HEAP_PREFERRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java index 54b6daf62..b932579ae 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java @@ -4,6 +4,7 @@ import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFE 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; @@ -82,7 +83,7 @@ public class NativeVelocityCompressor implements VelocityCompressor { } @Override - public boolean isNative() { - return true; + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_REQUIRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java index 5382da898..941f83cfb 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java @@ -1,6 +1,7 @@ package com.velocitypowered.natives.encryption; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; @@ -88,7 +89,7 @@ public class JavaVelocityCipher implements VelocityCipher { } @Override - public boolean isNative() { - return false; + public BufferPreference preferredBufferType() { + return BufferPreference.HEAP_PREFERRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java index b65501ff0..35de5519d 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java @@ -1,6 +1,7 @@ package com.velocitypowered.natives.encryption; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import java.security.GeneralSecurityException; @@ -81,7 +82,7 @@ public class NativeVelocityCipher implements VelocityCipher { } @Override - public boolean isNative() { - return true; + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_REQUIRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java b/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java new file mode 100644 index 000000000..7be4fa74e --- /dev/null +++ b/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java @@ -0,0 +1,16 @@ +package com.velocitypowered.natives.util; + +public enum BufferPreference { + /** + * A heap buffer is preferred (but not required). + */ + HEAP_PREFERRED, + /** + * A direct buffer is preferred (but not required). + */ + DIRECT_PREFERRED, + /** + * A direct buffer is required. + */ + DIRECT_REQUIRED +} diff --git a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java index c91e6c942..5bf328396 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java +++ b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java @@ -20,9 +20,8 @@ public class MoreByteBufUtils { * @return a buffer compatible with the native */ public static ByteBuf ensureCompatible(ByteBufAllocator alloc, Native nativeStuff, ByteBuf buf) { - if (!nativeStuff.isNative() || buf.hasMemoryAddress()) { - // Will always work in either case. JNI code demands a memory address, and if we have a Java - // fallback, it uses byte arrays in all cases. + if (nativeStuff.preferredBufferType() != BufferPreference.DIRECT_REQUIRED + || buf.hasMemoryAddress()) { return buf.retain(); } @@ -41,7 +40,8 @@ public class MoreByteBufUtils { * @return a buffer compatible with the native */ public static ByteBuf preferredBuffer(ByteBufAllocator alloc, Native nativeStuff) { - return nativeStuff.isNative() ? alloc.directBuffer() : alloc.heapBuffer(); + return nativeStuff.preferredBufferType() != BufferPreference.HEAP_PREFERRED + ? alloc.directBuffer() : alloc.heapBuffer(); } /** @@ -55,7 +55,7 @@ public class MoreByteBufUtils { */ public static ByteBuf preferredBuffer(ByteBufAllocator alloc, Native nativeStuff, int initialCapacity) { - return nativeStuff.isNative() ? alloc.directBuffer(initialCapacity) : alloc - .heapBuffer(initialCapacity); + return nativeStuff.preferredBufferType() != BufferPreference.HEAP_PREFERRED + ? alloc.directBuffer(initialCapacity) : alloc.heapBuffer(initialCapacity); } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java index 494b352ad..e4da84f73 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java @@ -15,7 +15,7 @@ public final class NativeCodeLoader implements Supplier { @Override public T get() { - return selected.object; + return selected.constructed; } private static Variant getVariant(List> variants) { @@ -38,9 +38,14 @@ public final class NativeCodeLoader implements Supplier { private Status status; private final Runnable setup; private final String name; - private final T object; + private final Supplier object; + private T constructed; Variant(BooleanSupplier possiblyAvailable, Runnable setup, String name, T object) { + this(possiblyAvailable, setup, name, () -> object); + } + + Variant(BooleanSupplier possiblyAvailable, Runnable setup, String name, Supplier object) { this.status = possiblyAvailable.getAsBoolean() ? Status.POSSIBLY_AVAILABLE : Status.NOT_AVAILABLE; this.setup = setup; @@ -57,6 +62,7 @@ public final class NativeCodeLoader implements Supplier { if (status == Status.POSSIBLY_AVAILABLE) { try { setup.run(); + constructed = object.get(); status = Status.SETUP; } catch (Exception e) { status = Status.SETUP_FAILURE; @@ -64,7 +70,7 @@ public final class NativeCodeLoader implements Supplier { } } - return object; + return constructed; } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java index bf5b73002..6e1bd1c3f 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java @@ -6,6 +6,7 @@ import java.util.function.BooleanSupplier; public class NativeConstraints { private static final boolean NATIVES_ENABLED = !Boolean.getBoolean("velocity.natives-disabled"); + private static final boolean IS_AMD64; private static final boolean CAN_GET_MEMORYADDRESS; static { @@ -15,19 +16,28 @@ public class NativeConstraints { } finally { test.release(); } + + String osArch = System.getProperty("os.arch", ""); + // HotSpot on Intel macOS prefers x86_64, but OpenJ9 on macOS and HotSpot/OpenJ9 elsewhere + // give amd64. + IS_AMD64 = osArch.equals("amd64") || osArch.equals("x86_64"); } static final BooleanSupplier MACOS = () -> { return NATIVES_ENABLED && CAN_GET_MEMORYADDRESS && System.getProperty("os.name", "").equalsIgnoreCase("Mac OS X") - && System.getProperty("os.arch", "").equals("x86_64"); + && IS_AMD64; }; static final BooleanSupplier LINUX = () -> { return NATIVES_ENABLED && CAN_GET_MEMORYADDRESS && System.getProperty("os.name", "").equalsIgnoreCase("Linux") - && System.getProperty("os.arch", "").equals("amd64"); + && IS_AMD64; + }; + + static final BooleanSupplier JAVA_11 = () -> { + return Double.parseDouble(System.getProperty("java.specification.version")) >= 11; }; } diff --git a/native/src/main/java/com/velocitypowered/natives/util/Natives.java b/native/src/main/java/com/velocitypowered/natives/util/Natives.java index 44329434a..7e4f05e97 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/Natives.java +++ b/native/src/main/java/com/velocitypowered/natives/util/Natives.java @@ -2,6 +2,7 @@ package com.velocitypowered.natives.util; 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.VelocityCompressorFactory; @@ -52,6 +53,8 @@ public class Natives { new NativeCodeLoader.Variant<>(NativeConstraints.LINUX, copyAndLoadNative("/linux_x64/velocity-compress.so"), "native (Linux amd64)", NativeVelocityCompressor.FACTORY), + new NativeCodeLoader.Variant<>(NativeConstraints.JAVA_11, () -> { + }, "Java 11", () -> Java11VelocityCompressor.FACTORY), new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> { }, "Java", JavaVelocityCompressor.FACTORY) ) diff --git a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java index 40f74999e..182423e98 100644 --- a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java +++ b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.condition.OS.LINUX; import static org.junit.jupiter.api.condition.OS.MAC; +import com.velocitypowered.natives.util.BufferPreference; import com.velocitypowered.natives.util.Natives; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -17,7 +18,9 @@ import java.util.zip.DataFormatException; import java.util.zip.Deflater; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.JRE; class VelocityCompressorTest { @@ -39,7 +42,7 @@ class VelocityCompressorTest { @EnabledOnOs({MAC, LINUX}) void nativeIntegrityCheck() throws DataFormatException { VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION); - if (compressor instanceof JavaVelocityCompressor) { + if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) { compressor.dispose(); fail("Loaded regular compressor"); } @@ -60,6 +63,22 @@ class VelocityCompressorTest { check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); } + @Test + @EnabledOnJre(JRE.JAVA_11) + void java11IntegrityCheckDirect() throws DataFormatException { + VelocityCompressor compressor = Java11VelocityCompressor.FACTORY + .create(Deflater.DEFAULT_COMPRESSION); + check(compressor, () -> Unpooled.directBuffer(TEST_DATA.length + 32)); + } + + @Test + @EnabledOnJre(JRE.JAVA_11) + void java11IntegrityCheckHeap() throws DataFormatException { + VelocityCompressor compressor = Java11VelocityCompressor.FACTORY + .create(Deflater.DEFAULT_COMPRESSION); + check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); + } + private void check(VelocityCompressor compressor, Supplier bufSupplier) throws DataFormatException { ByteBuf source = bufSupplier.get(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java index 87fb83524..aaa0a8643 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java @@ -17,7 +17,7 @@ public class VelocityNettyThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { - String name = String.format(nameFormat, threadNumber.incrementAndGet()); + String name = String.format(nameFormat, threadNumber.getAndIncrement()); return new FastThreadLocalThread(r, name); } }