Skip to content

Migrate benchmarks to kotlinx-benchmark for JVM+Native#512

Draft
andrewparmet wants to merge 25 commits intoopen-toast:mainfrom
andrewparmet:native-benchmarks
Draft

Migrate benchmarks to kotlinx-benchmark for JVM+Native#512
andrewparmet wants to merge 25 commits intoopen-toast:mainfrom
andrewparmet:native-benchmarks

Conversation

@andrewparmet
Copy link
Copy Markdown
Collaborator

@andrewparmet andrewparmet commented Apr 14, 2026

  • Migrate from JMH to kotlinx-benchmark, which delegates to JMH on JVM and uses its own runtime on Kotlin/Native
  • Add macosArm64 native benchmark target alongside JVM
  • Consolidate benchmark code into a shared multiplatform interface with JVM-specific streaming extensions
  • Enable PersistentCollectionFactory parameterization on both JVM and Native
  • Update RESULTS.md with fresh benchmark data for protokt JVM, protokt Native, and protobuf-java (Wire not re-run yet)

Highlights from results

  • On JVM, PersistentCollectionFactory shows no measurable overhead vs DefaultCollectionFactory on any benchmark
  • On Native, persistent collections provide 95-99% speedup on large-collection copy/append operations
  • On Native, persistent collections add 43-110% overhead to deserialization from scratch
  • protokt serialization remains ~4x faster than protobuf-java on large messages

Test plan

  • JVM benchmarks pass (118 runs, 39 methods)
  • Native benchmarks pass (68 runs, 34 methods)
  • protobuf-java benchmarks pass (39 runs, 39 methods)
  • Wire benchmarks (paused, not re-run yet)
  • CI

@andrewparmet andrewparmet force-pushed the native-benchmarks branch 3 times, most recently from ad76523 to 48c0e25 Compare April 14, 2026 15:47
New commonMain benchmarks run on both JVM (backed by JMH) and Native,
covering all the same scenarios as the existing JMH benchmarks:
deserialize, serialize, passthrough, mutate-and-serialize, copy-append,
and string-heavy workloads.

Streaming benchmarks use kotlinx.io.Buffer directly (works on all
platforms) instead of ByteArrayOutputStream.

Existing JMH benchmarks preserved in jvmMain for the legacy runner
and protobuf-java/Wire comparison modules.

Run:
  ./gradlew :benchmarks:protokt-benchmarks:macosArm64Benchmark
  ./gradlew :benchmarks:protokt-benchmarks:jvmBenchmark
- benchmarks-util: multiplatform module with ProtobufBenchmarkSet
  interface (kotlinx-benchmark Blackhole), randomUtf8String (kotlin
  Random), readDatasetBytes expect/actual
- JvmProtobufBenchmarkSet: extends base with InputStream-streaming
  methods for protobuf-java and Wire
- Remove duplicate ProtoktBenchmarks.kt from jvmMain (commonMain
  version covers all benchmarks)
- protobuf-java/Wire benchmarks: switch to kotlin.random.Random,
  implement JvmProtobufBenchmarkSet
…terface

- ProtobufBenchmarkSet<T>: generic over blackhole type, avoids
  kotlinx-benchmark runtime resolution issues in the util module
- All annotations switched from org.openjdk.jmh to kotlinx.benchmark
- Shared benchmark config via configureBenchmarks() with Gradle
  property overrides (-PbenchmarkInclude, -PbenchmarkExclude, etc.)
- Removed legacy JMH runner, main() functions, kapt, application plugin
- protobuf-java and Wire: register("main") benchmark target for JVM
- README updated with unified API
- Add allopen plugin for @State-annotated classes (JMH requires open
  classes and methods); remove manual `open` keyword from all benchmark
  classes
- benchmarks-conventions: allopen + kotlinx-benchmark-runtime, drop kapt
  (kotlinx-benchmark handles JMH annotation processing)
- protobuf-java/Wire: register("main") target, kotlinx.benchmark
  annotations, remove legacy JMH main() functions
- protokt-benchmarks: add allopen for JVM target, remove kapt
- BenchmarkConfiguration: add jmhIgnoreLock to prevent stale lock issues
- Restore 20K string comment, clean up buildSrc deps, remove stray files
- Merge DatasetReader + RandomUtf8 into BenchmarkUtils.kt (commonMain)
- Keep ProtobufBenchmarkSet and JvmProtobufBenchmarkSet as own files
- Add ProtoktJvmBenchmarks with InputStream-based streaming
  deserialization benchmarks (deserializeLargeStreaming, etc.)
- Fix kotlin.random.Random import
- @param for collectionFactory in commonMain benchmarks
- expect/actual applyBenchmarkConfig: JVM uses System.setProperty,
  native sets collectionFactoryOverride directly via friend paths
- runtimeFriendPaths() on benchmark module for internal access
- persistent-collections dependency moved to commonMain
…le native collection factory parameterization
ftell returns Int on mingwX64 making .toInt() redundant, but it's needed
on other native targets where ftell returns Long.
}
}

fun Project.friendPaths(vararg friendProjectPaths: String) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since native can't use system properties to configure codec/collection factory, we have to use friend paths now. This structure decouples friend path declaration from the runtime module so we can expose the internal members necessary to set codec/coll. fac. from the benchmark projects.

… targets

Add @param for codec to ProtoktMultiplatformBenchmarks so JVM results
break down by codec (matching RESULTS.md). On native the codec param
iterates but has no effect (single built-in codec). Also add
enableNativeTargets() to benchmark modules for macosArm64 execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant