From 3682a257638108f521b437a25b70ade1033249b2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:42:57 +0100 Subject: [PATCH 1/7] Add JDA integration --- gradle/libs.versions.toml | 3 + jda-integration/build.gradle.kts | 62 ++++++++++++++++ ...dBufferedTransportGatewayDecompressor.java | 52 ++++++++++++++ ...dStreamedTransportGatewayDecompressor.java | 72 +++++++++++++++++++ ...eamedTransportGatewayDecompressorTest.java | 41 +++++++++++ settings.gradle.kts | 1 + 6 files changed, 231 insertions(+) create mode 100644 jda-integration/build.gradle.kts create mode 100644 jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java create mode 100644 jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java create mode 100644 jda-integration/src/test/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressorTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a16dc9f..0052022 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [libraries] +assertj = "org.assertj:assertj-core:3.27.6" jackson-databind = "tools.jackson.core:jackson-databind:3.0.3" jda = "net.dv8tion:JDA:6.1.0" +jda-snapshot = "net.dv8tion:JDA:6.2.0_DEV" jna = "net.java.dev.jna:jna:5.18.1" jreleaser = "org.jreleaser:org.jreleaser.gradle.plugin:1.20.0" jspecify = "org.jspecify:jspecify:1.0.0" @@ -8,6 +10,7 @@ junit = "org.junit.jupiter:junit-jupiter:5.14.0" junit-launcher = "org.junit.platform:junit-platform-launcher:1.14.0" kotlin-logging-jvm = "io.github.oshai:kotlin-logging-jvm:7.0.13" kotlinx-coroutines-core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2" +mockito = "org.mockito:mockito-core:5.20.0" slf4j = "org.slf4j:slf4j-api:2.0.17" logback-classic = "ch.qos.logback:logback-classic:1.5.21" trove4j-core = "net.sf.trove4j:core:3.1.0" diff --git a/jda-integration/build.gradle.kts b/jda-integration/build.gradle.kts new file mode 100644 index 0000000..d4cf825 --- /dev/null +++ b/jda-integration/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + `java-conventions` + `java-library` + `publish-conventions` +} + +val fullProjectName = "${rootProject.name}-${project.name}" + +tasks.withType { + archiveBaseName = fullProjectName +} + +repositories { + mavenLocal() +} + +val mockitoAgent by configurations.creating +dependencies { + implementation(project(":api")) + runtimeOnly(project(":jni-impl")) + + implementation(libs.jda.snapshot) + + //Code safety + compileOnly(libs.jspecify) + testCompileOnly(libs.jspecify) + + //Logger + implementation(libs.slf4j) + + // JUnit 5 (JUnit 6 is not Java 8 compatible) + testImplementation(libs.bundles.junit) + testImplementation(libs.mockito) + mockitoAgent(libs.mockito) { isTransitive = false } + testImplementation(libs.assertj) + + testRuntimeOnly(libs.logback.classic) +} + +java { + withJavadocJar() + withSourcesJar() +} + +tasks.named("compileJava") { + options.release.set(8) +} + +tasks.test { + useJUnitPlatform() + failFast = false + + jvmArgs("-javaagent:${mockitoAgent.asPath}") +} + +registerPublication( + name = fullProjectName, + description = "Lightweight Zstandard decompressor for JDA", + url = "https://github.com/freya022/discord-zstd-java/tree/master/jda-integration", +) { + from(components["java"]) +} diff --git a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java new file mode 100644 index 0000000..dda1e0b --- /dev/null +++ b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java @@ -0,0 +1,52 @@ +package dev.freya02.discord.zstd.jda; + +import dev.freya02.discord.zstd.api.DiscordZstdDecompressor; +import dev.freya02.discord.zstd.api.DiscordZstdException; +import net.dv8tion.jda.api.exceptions.DecompressionException; +import net.dv8tion.jda.api.requests.gateway.compression.GatewayDecompressor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +@NullMarked +public class ZstdBufferedTransportGatewayDecompressor implements GatewayDecompressor.Transport.Buffered { + private static final Logger LOGGER = LoggerFactory.getLogger(ZstdBufferedTransportGatewayDecompressor.class); + + private final DiscordZstdDecompressor decompressor; + + public ZstdBufferedTransportGatewayDecompressor(DiscordZstdDecompressor decompressor) { + this.decompressor = decompressor; + } + + @Nullable + @Override + public String getQueryParameter() { + return "zstd-stream"; + } + + @Override + public void reset() { + decompressor.reset(); + } + + @Override + public void shutdown() { + decompressor.close(); + } + + @Override + public byte[] decompress(byte[] data) throws DecompressionException { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Decompressing data {}", Arrays.toString(data)); + } + + try { + return decompressor.decompress(data); + } catch (DiscordZstdException e) { + throw new DecompressionException(e); + } + } +} diff --git a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java new file mode 100644 index 0000000..0ba5d3a --- /dev/null +++ b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java @@ -0,0 +1,72 @@ +package dev.freya02.discord.zstd.jda; + +import dev.freya02.discord.zstd.api.DiscordZstdContext; +import dev.freya02.discord.zstd.api.DiscordZstdException; +import net.dv8tion.jda.api.exceptions.DecompressionException; +import net.dv8tion.jda.api.requests.gateway.compression.GatewayDecompressor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +@NullMarked +public class ZstdStreamedTransportGatewayDecompressor implements GatewayDecompressor.Transport.Streamed { + private static final Logger LOGGER = LoggerFactory.getLogger(ZstdStreamedTransportGatewayDecompressor.class); + + private final DiscordZstdContext context; + + public ZstdStreamedTransportGatewayDecompressor(DiscordZstdContext context) { + this.context = context; + } + + @Nullable + @Override + public String getQueryParameter() { + return "zstd-stream"; + } + + @Override + public void reset() { + context.reset(); + } + + @Override + public void shutdown() { + context.close(); + } + + @Override + public InputStream createInputStream(byte[] data) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Decompressing data {}", Arrays.toString(data)); + } + + return new GatewayInputStream(context.createInputStream(data)); + } + + private static class GatewayInputStream extends FilterInputStream { + + private GatewayInputStream(InputStream in) { + super(in); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + return super.read(b, off, len); + } catch (IOException e) { + Throwable cause = e.getCause(); + if (cause instanceof DiscordZstdException) { + throw new DecompressionException(e); + } + + throw e; + } + } + } +} diff --git a/jda-integration/src/test/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressorTest.java b/jda-integration/src/test/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressorTest.java new file mode 100644 index 0000000..f0173ff --- /dev/null +++ b/jda-integration/src/test/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressorTest.java @@ -0,0 +1,41 @@ +package dev.freya02.discord.zstd.jda; + +import dev.freya02.discord.zstd.api.DiscordZstdContext; +import dev.freya02.discord.zstd.api.DiscordZstdException; +import net.dv8tion.jda.api.exceptions.DecompressionException; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.io.InputStream; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +@NullMarked +public class ZstdStreamedTransportGatewayDecompressorTest { + @SuppressWarnings({"resource", "ResultOfMethodCallIgnored"}) + @Test + public void testDecompressionErrorThrowsDecompressionException() { + var context = Mockito.mock(DiscordZstdContext.class); + + var stream = new InputStream() { + @Override + public int read() { + throw new UnsupportedOperationException(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + throw new IOException("Must be unwrapped", new DiscordZstdException("Expected")); + } + }; + doReturn(stream).when(context).createInputStream(any()); + + var decompressor = new ZstdStreamedTransportGatewayDecompressor(context); + + assertThatExceptionOfType(DecompressionException.class).isThrownBy(() -> decompressor.createInputStream(new byte[0]).read(new byte[0])); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 76008de..0bae8f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,3 +9,4 @@ include(":test-data") include(":test-data-generator") include(":benchmarks", ":benchmarks:results-converter") include(":live-metrics-processor") +include(":jda-integration") From 4e09745ebe62fbf01f6abb456eb51a2f8b319324 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:10:35 +0100 Subject: [PATCH 2/7] Add supplier factory methods --- ...dBufferedTransportGatewayDecompressor.java | 39 ++++++++++++++++++- ...dStreamedTransportGatewayDecompressor.java | 18 +++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java index dda1e0b..9bede8b 100644 --- a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java +++ b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdBufferedTransportGatewayDecompressor.java @@ -1,7 +1,6 @@ package dev.freya02.discord.zstd.jda; -import dev.freya02.discord.zstd.api.DiscordZstdDecompressor; -import dev.freya02.discord.zstd.api.DiscordZstdException; +import dev.freya02.discord.zstd.api.*; import net.dv8tion.jda.api.exceptions.DecompressionException; import net.dv8tion.jda.api.requests.gateway.compression.GatewayDecompressor; import org.jspecify.annotations.NullMarked; @@ -10,7 +9,13 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.function.Supplier; +/** + * Provides buffered transport-level decompression of gateway messages for the Java Discord API (JDA). + * + * @see #supplier(int) + */ @NullMarked public class ZstdBufferedTransportGatewayDecompressor implements GatewayDecompressor.Transport.Buffered { private static final Logger LOGGER = LoggerFactory.getLogger(ZstdBufferedTransportGatewayDecompressor.class); @@ -21,6 +26,36 @@ public ZstdBufferedTransportGatewayDecompressor(DiscordZstdDecompressor decompre this.decompressor = decompressor; } + /** + * Creates a supplier of {@link ZstdBufferedTransportGatewayDecompressor} with the provided decompression buffer size. + * + *

Buffer sizes

+ * This defines the size, in bytes, of the intermediate buffer used for decompression, + * larger buffer means less decompression loops at a fixed cost of memory. + * + * + * + * @param bufferSizeHint + * The hint or value for the size of the buffer used for decompression + * + * @throws IllegalArgumentException + * If {@code bufferSize} is less than {@value DiscordZstdDecompressor#MIN_BUFFER_SIZE} and not {@value DiscordZstdDecompressor#ZSTD_RECOMMENDED_BUFFER_SIZE} + * + * @return A new supplier of {@link ZstdBufferedTransportGatewayDecompressor} + */ + public static Supplier supplier(int bufferSizeHint) { + DiscordZstd zstd = DiscordZstdProvider.get(); + DiscordZstdDecompressorFactory factory = zstd.createDecompressorFactory(bufferSizeHint); + return () -> new ZstdBufferedTransportGatewayDecompressor(factory.create()); + } + @Nullable @Override public String getQueryParameter() { diff --git a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java index 0ba5d3a..7888945 100644 --- a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java +++ b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java @@ -1,7 +1,9 @@ package dev.freya02.discord.zstd.jda; +import dev.freya02.discord.zstd.api.DiscordZstd; import dev.freya02.discord.zstd.api.DiscordZstdContext; import dev.freya02.discord.zstd.api.DiscordZstdException; +import dev.freya02.discord.zstd.api.DiscordZstdProvider; import net.dv8tion.jda.api.exceptions.DecompressionException; import net.dv8tion.jda.api.requests.gateway.compression.GatewayDecompressor; import org.jspecify.annotations.NullMarked; @@ -13,7 +15,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.function.Supplier; +/** + * Provides streamed transport-level decompression of gateway messages for the Java Discord API (JDA). + * + * @see #supplier() + */ @NullMarked public class ZstdStreamedTransportGatewayDecompressor implements GatewayDecompressor.Transport.Streamed { private static final Logger LOGGER = LoggerFactory.getLogger(ZstdStreamedTransportGatewayDecompressor.class); @@ -24,6 +32,16 @@ public ZstdStreamedTransportGatewayDecompressor(DiscordZstdContext context) { this.context = context; } + /** + * Creates a supplier of {@link ZstdStreamedTransportGatewayDecompressor}. + * + * @return A new supplier of {@link ZstdStreamedTransportGatewayDecompressor} + */ + public static Supplier supplier() { + DiscordZstd zstd = DiscordZstdProvider.get(); + return () -> new ZstdStreamedTransportGatewayDecompressor(zstd.createContext()); + } + @Nullable @Override public String getQueryParameter() { From 8a459de6f329a5c2a04401b34f2a04fc955539a5 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:11:08 +0100 Subject: [PATCH 3/7] Expose API module As docs refer to some of their classes --- jda-integration/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jda-integration/build.gradle.kts b/jda-integration/build.gradle.kts index d4cf825..fd015be 100644 --- a/jda-integration/build.gradle.kts +++ b/jda-integration/build.gradle.kts @@ -16,7 +16,7 @@ repositories { val mockitoAgent by configurations.creating dependencies { - implementation(project(":api")) + api(project(":api")) runtimeOnly(project(":jni-impl")) implementation(libs.jda.snapshot) From f29079092dfed1483d2f60e3a6ea1d45d1d38f18 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:16:44 +0100 Subject: [PATCH 4/7] Nit --- .../zstd/jda/ZstdStreamedTransportGatewayDecompressor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java index 7888945..488dbe2 100644 --- a/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java +++ b/jda-integration/src/main/java/dev/freya02/discord/zstd/jda/ZstdStreamedTransportGatewayDecompressor.java @@ -76,7 +76,7 @@ private GatewayInputStream(InputStream in) { @Override public int read(byte[] b, int off, int len) throws IOException { try { - return super.read(b, off, len); + return in.read(b, off, len); } catch (IOException e) { Throwable cause = e.getCause(); if (cause instanceof DiscordZstdException) { From a76ce13ff6ad699e5eeec1c76f94c4a342f8dd0f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:45:22 +0100 Subject: [PATCH 5/7] Update READMEs --- README.md | 55 ++++++++++++----------------------- jda-integration/README.md | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 jda-integration/README.md diff --git a/README.md b/README.md index ab712ef..1537f93 100644 --- a/README.md +++ b/README.md @@ -12,54 +12,35 @@ Lightweight modular support for Zstandard streaming decompression, for JVM Disco - Windows: x86-64, aarch64 - macOS (darwin): x86-64, aarch64 -## 🤖 For bot developers +## 🔥 For JDA users -### Installation +See the [JDA integration module](jda-integration). -You're likely here if you want to use Zstd decompression for your Discord bot! - -[![discord-zstd-java-jni-impl on Maven Central][jni-impl-maven-central-shield] ][jni-impl-maven-central-link] - -This is compatible with Java 8+. - -#### Gradle -```kotlin -dependencies { - runtimeOnly("dev.freya02:discord-zstd-java-jni-impl:VERSION") // TODO replace VERSION with current release -} -``` - -#### Maven -```xml - - dev.freya02 - discord-zstd-java-jni-impl - VERSION - runtime - -``` - -> [!TIP] -> To remove the warning when the natives are loaded, add `--enable-native-access=ALL-UNNAMED` to your JVM arguments. +## 📖 For library developers -### Usage -As a bot developer, you don't need to do anything. +[![discord-zstd-java-api on Maven Central][api-maven-central-shield] ][api-maven-central-link] -If you want to load a different version of the native library, -you can do so by calling `ZstdNativesLoader.load(Path)` or `loadFromJar(String)`. These functions will return `false` if the natives were already loaded, as they can't be replaced. +### Built-in integration -## 📖 For library developers -### Installation +If you decide to integrate this library into yours, +you will only need the `dev.freya02:discord-zstd-java-api:VERSION` dependency, it is compatible with Java 8+. -[![discord-zstd-java-api on Maven Central][api-maven-central-shield] ][api-maven-central-link] +Your users will need to install an implementation, we recommend using `discord-zstd-java-jni-impl`. -You will only need the `dev.freya02:discord-zstd-java-api:VERSION` dependency, it is compatible with Java 8+. +The users can also load a different version of the native library, +they can do so by calling `ZstdNativesLoader.load(Path)` or `loadFromJar(String)`. +These functions will return `false` if the natives were already loaded, as they can't be replaced. -### Usage +#### Usage The main interface is `DiscordZstd`, you can get an instance with `DiscordZstdProvider.get()`. Then, you can either: 1. Do bulk processing with a decompressor obtained with `DiscordZstd#createDecompressor` and kept per gateway connection, calling `ZstdDecompressor#decompress` on each gateway message 2. Process gradually with a context obtained from `DiscordZstd#createContext` and kept per gateway connection, - then making input streams with `ZstdContext#createInputStream` from each gateway message + then making input streams with `ZstdContext#createInputStream` from each gateway message + +### External integration + +You can also provide an API in your library, this way users can choose to use any decompression library they want. +After creating your API, please submit a pull request here with a new module which implements that API. diff --git a/jda-integration/README.md b/jda-integration/README.md new file mode 100644 index 0000000..da2fbde --- /dev/null +++ b/jda-integration/README.md @@ -0,0 +1,61 @@ +[api-maven-central-shield]: https://img.shields.io/maven-central/v/dev.freya02/discord-zstd-java-api?label=Maven%20central&logo=apachemaven +[api-maven-central-link]: https://central.sonatype.com/artifact/dev.freya02/discord-zstd-java-api +[jda-integration-maven-central-shield]: https://img.shields.io/maven-central/v/dev.freya02/discord-zstd-java-jda-integration?label=Maven%20central&logo=apachemaven +[jda-integration-maven-central-link]: https://central.sonatype.com/artifact/dev.freya02/discord-zstd-java-jda-integration + +# discord-zstd-java - JDA integration + +Lightweight Zstandard decompression for the Java Discord API. (JDA) + +## Installation + +[![discord-zstd-java-jda-integration on Maven Central][jda-integration-maven-central-shield] ][jda-integration-maven-central-link] + +This is compatible with Java 8+. + +### Gradle +```kotlin +dependencies { + implementation("dev.freya02:discord-zstd-java-jda-integration:VERSION") // TODO replace VERSION with current release +} +``` + +### Maven +```xml + + dev.freya02 + discord-zstd-java-jda-integration + VERSION + +``` + +> [!TIP] +> To remove the warning when the natives are loaded, add `--enable-native-access=ALL-UNNAMED` to your JVM arguments. + +## Usage + +JDA's gateway decompressor can be configured in `GatewayConfig.Builder`, in two ways: + +- `useBufferedTransportDecompression` lets you use decompress payloads all at once, this is what JDA does by default. +- `useStreamedTransportDecompression` lets you decompress payloads progressively, this means no memory allocations for decompression, but also prevents from printing corrupted payloads (though extremely rare) + +If you choose to use buffered decompression, use `ZstdBufferedTransportGatewayDecompressor`, if you want to use streamed decompression, use `ZstdStreamedTransportGatewayDecompressor`. + +For example: + +```java +void main(String[] args) { + DefaultShardManagerBuilder + .createDefault(args[0]) + .setGatewayConfig( + GatewayConfig.builder() + .useStreamedTransportDecompression(ZstdStreamedTransportGatewayDecompressor.supplier()) + .build()) + .build(); +} +``` + +## Overriding natives + +If you want to load a different version of the native library, +you can do so by calling `ZstdNativesLoader.load(Path)` or `loadFromJar(String)`. These functions will return `false` if the natives were already loaded, as they can't be replaced. From eda47b20645c5eed38c1af12d3da251b647b62cd Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:56:09 +0100 Subject: [PATCH 6/7] Add benchmark notes --- jda-integration/BENCHMARKS.md | 44 +++++++++++++++++++++++++++++++++++ jda-integration/README.md | 2 ++ 2 files changed, 46 insertions(+) create mode 100644 jda-integration/BENCHMARKS.md diff --git a/jda-integration/BENCHMARKS.md b/jda-integration/BENCHMARKS.md new file mode 100644 index 0000000..61d5534 --- /dev/null +++ b/jda-integration/BENCHMARKS.md @@ -0,0 +1,44 @@ +# Benchmarks - Compared to JDA's Zlib decompression + +## Overview + +Zstd decompression is beneficial for larger bots where the CPU and GC allocations improvements will be the most noticeable, and for Discord itself. Smaller bots will only see slight benefits. + +In both cases, payloads are slightly smaller mostly during startup, and quite similar during normal operation. + +In the best case, when decompressing and deserializing, we see a 31% speed improvement with half the memory allocations. + +## Synthetic benchmarks + +> [!NOTE] +> Thanks to @MrPowerGamerBR for helping me gather some data from their bot and running benchmarks on their machine! + +> [!TIP] +> "bulk" here means going from compressed `byte[]` to a fully decompressed `byte[]`. + +With 10 shards starting up from scratch, giving us 364 MB of decompressed data, they ran [a few benchmarks](../benchmarks/src/jmh/java/dev/freya02/discord/zstd) comparing zlib, zstd, as well as their bulk (what JDA currently does) and stream (letting Jackson consume an `InputStream`) variants. + +They were run on their production server, with an AMD Ryzen 5 5600X and produced [this data](https://gist.github.com/freya022/0516a809d43ee1d084ed205ac4fbe56c). + +If we look at the complete package, transforming the compressed data into a usable `DataObject`, we can see bulk-decompressing with Zstd cut time spent by 30% and reduced GC allocations by 35% + +However, if we use streaming, Zlib gets about 10% of speed increase and removes 48% of GC allocations, while Zstd gets a 31% speed improvement and the same memory improvements. + +### Other benchmarks + +I have also run some numbers on my machine (an AMD Ryzen 7 3700X, with boost disabled), not much different here: + +- 10 shards of [randomly generated](../test-data-generator) data: https://gist.github.com/freya022/04d5e8cf7d44c9680ae42154808cddfd +- A bot with a single guild: https://gist.github.com/freya022/8922140965bc51a699b135ebc2f96914 + +## Runtime statistics + +They were also kind enough to run their ~2.5K shards for a day while recording the compressed size, decompressed size and time-to-decompress. + +The shards were split in 3 equally-sized sets: + +- Using the current Zlib implementation +- Using Zstd with a 8 KB buffer +- Using Zstd with a 128 KB buffer + +The results can be seen there: https://gist.github.com/freya022/7b35aa412a4f125ca1b139b71360ab45 diff --git a/jda-integration/README.md b/jda-integration/README.md index da2fbde..5600f85 100644 --- a/jda-integration/README.md +++ b/jda-integration/README.md @@ -7,6 +7,8 @@ Lightweight Zstandard decompression for the Java Discord API. (JDA) +This module is typically useful for sharded (large) bots, if you are interested in the performance difference, see [here](BENCHMARKS.md). + ## Installation [![discord-zstd-java-jda-integration on Maven Central][jda-integration-maven-central-shield] ][jda-integration-maven-central-link] From b0067a78f507d9e825593fcad1e5c3e2ed26c4e6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:59:36 +0100 Subject: [PATCH 7/7] Add automatic module name --- jda-integration/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jda-integration/build.gradle.kts b/jda-integration/build.gradle.kts index fd015be..7eff23a 100644 --- a/jda-integration/build.gradle.kts +++ b/jda-integration/build.gradle.kts @@ -53,6 +53,12 @@ tasks.test { jvmArgs("-javaagent:${mockitoAgent.asPath}") } +tasks.jar { + manifest { + attributes("Automatic-Module-Name" to "discord.zstd.java.jda.integration") + } +} + registerPublication( name = fullProjectName, description = "Lightweight Zstandard decompressor for JDA",