From f00743a9223341503db910a2fae4e1d7db473dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Sun, 17 May 2026 10:08:20 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20benchmark=20infrastructure=20?= =?UTF-8?q?=E2=80=94=20JVM=20heap,=20Liquibase,=20MockProducer=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent fixes that make ./gradlew :okapi-benchmarks:jmh complete cleanly. Before these, the JMH run OOMs partway through. 1. Bump JMH JVM heap from -Xmx2g to -Xmx8g. Throughput-mode microbenchmarks call deliver() at ~1M ops/s; each call allocates Jackson + Kotlin reflection state for deserialization. At 2GB the allocation rate exceeds GC throughput and OOMs within the first measurement iteration. 2. Pass -Dliquibase.duplicateFileMode=WARN as JMH JVM arg. okapi-postgres.jar and the fat JMH jar both ship the changelog at the same classpath path. Liquibase 4.x treats duplicate resources as an error by default, which aborts PostgresBenchmarkSupport setup. The two files are identical (same jar source on the classpath twice), so WARN is safe. 3. Subclass MockProducer in DelivererMicroBenchmark to clear() history after every send(). MockProducer.history (internal `sent` list) retains every record sent for inspection — there is no eviction. In throughput mode at ~1M ops/s for 30s × forks × iterations that list grew to GBs and OOMed the JVM regardless of heap size. Discarding per call is safe because microbench doesn't inspect what was sent — only timing. All three issues exist on main today; running the benchmark suite without these fixes will fail. No test or production code is touched. --- okapi-benchmarks/build.gradle.kts | 14 +++++++++++++- .../okapi/benchmarks/DelivererMicroBenchmark.kt | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/okapi-benchmarks/build.gradle.kts b/okapi-benchmarks/build.gradle.kts index d1c27b0..51eac44 100644 --- a/okapi-benchmarks/build.gradle.kts +++ b/okapi-benchmarks/build.gradle.kts @@ -41,7 +41,19 @@ jmh { timeOnIteration = "30s" resultFormat = "JSON" resultsFile = layout.buildDirectory.file("reports/jmh/results.json") - jvmArgs = listOf("-Xms2g", "-Xmx2g", "-XX:+UseG1GC") + jvmArgs = listOf( + // Throughput-mode microbenchmarks call deliver() in a tight loop and re-deserialize + // KafkaDeliveryInfo via Jackson + Kotlin reflection per invocation; with -Xmx2g this + // OOMs within the first measurement iteration. 8g leaves room for GC under sustained + // allocation pressure without skewing the benchmark with promotion stalls. + "-Xms8g", + "-Xmx8g", + "-XX:+UseG1GC", + // okapi-postgres.jar and the fat JMH jar both end up on the classpath; both carry + // the Liquibase changelog. Liquibase 4.x treats this as an error by default. The + // files are identical (same jar source on the classpath twice), so WARN is safe. + "-Dliquibase.duplicateFileMode=WARN", + ) } // ktlint should not lint JMH-generated sources. diff --git a/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt b/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt index 81c258b..9d830b7 100644 --- a/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt +++ b/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt @@ -15,7 +15,10 @@ import com.softwaremill.okapi.http.ServiceUrlResolver import com.softwaremill.okapi.kafka.KafkaDeliveryInfo import com.softwaremill.okapi.kafka.KafkaMessageDeliverer import org.apache.kafka.clients.producer.MockProducer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.clients.producer.RecordMetadata import org.apache.kafka.common.serialization.StringSerializer +import java.util.concurrent.Future import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Mode @@ -52,7 +55,18 @@ open class DelivererMicroBenchmark { @Setup(org.openjdk.jmh.annotations.Level.Trial) fun setupTrial() { - val mockProducer = MockProducer(true, null, StringSerializer(), StringSerializer()) + // MockProducer.send() appends every record to an internal `sent` list (exposed as + // history()) and never drops it. In throughput-mode at ~1M ops/s for 30s × multiple + // iterations × forks that list grows to GBs and OOMs the JVM regardless of -Xmx. + // Override send() to discard history after each call — for microbench we don't need + // to inspect what was sent, only to measure deliver() overhead. + val mockProducer = object : MockProducer(true, null, StringSerializer(), StringSerializer()) { + override fun send(record: ProducerRecord): Future { + val future = super.send(record) + clear() + return future + } + } kafkaDeliverer = KafkaMessageDeliverer(mockProducer) wiremock = WireMockServer(wireMockConfig().dynamicPort()).also { it.start() } From 941513c6e724cadafa005e98f458a6f9a0e68bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Sun, 17 May 2026 10:14:00 +0200 Subject: [PATCH 2/2] style: fix import order in DelivererMicroBenchmark per ktlint --- .../softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt b/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt index 9d830b7..8b74742 100644 --- a/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt +++ b/okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/DelivererMicroBenchmark.kt @@ -18,7 +18,6 @@ import org.apache.kafka.clients.producer.MockProducer import org.apache.kafka.clients.producer.ProducerRecord import org.apache.kafka.clients.producer.RecordMetadata import org.apache.kafka.common.serialization.StringSerializer -import java.util.concurrent.Future import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Mode @@ -28,6 +27,7 @@ import org.openjdk.jmh.annotations.Setup import org.openjdk.jmh.annotations.State import org.openjdk.jmh.annotations.TearDown import java.time.Instant +import java.util.concurrent.Future import java.util.concurrent.TimeUnit /**