Skip to content
Back to skills

Kmp Benchmark

ASecurity

Sets up kotlinx-benchmark for Kotlin Multiplatform performance measurement. Covers: Gradle plugin + allopen wiring, a separate benchmark source set (never mixed into commonMain/commonTest), @State/@Benchmark/@Setup/@TearDown conventions, per-target registration (JVM/Native/JS/Wasm), running benchmarks, and reading JSON/CSV output. Use this whenever a performance claim needs a real number instead of a guess — this is the "profile first" step the kmp-expert performance decision tree routes to w...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 2, 2026
ai-agentsgokotlinbashnodenodejstestinggitapiperformance

Works with

  • api

Security analysis

A100/100

Scanned September 2, 2026

npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-benchmark --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kmp Benchmark?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kmp Benchmark
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ronjunevaldoz-kmp-benchmark/badge)](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-benchmark)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kmp-benchmark
description: >
  Sets up kotlinx-benchmark for Kotlin Multiplatform performance measurement. Covers:
  Gradle plugin + allopen wiring, a separate benchmark source set (never mixed into
  commonMain/commonTest), @State/@Benchmark/@Setup/@TearDown conventions, per-target
  registration (JVM/Native/JS/Wasm), running benchmarks, and reading JSON/CSV output.
  Use this whenever a performance claim needs a real number instead of a guess — this is
  the "profile first" step the kmp-expert performance decision tree
  routes to when a request is unnamed or needs a concrete target before further routing.
license: Apache-2.0
metadata:
  author: kmp-agent-skills
  last-updated: '2026-07-10'
  keywords:
    - kotlinx-benchmark
    - benchmark
    - microbenchmark
    - performance measurement
    - JMH
    - allopen
    - Benchmark annotation
    - profile performance
    - performance number
---

## When to Use This Skill

Use when you need to:
- Get a real, reproducible number before claiming a change "improves performance"
- Compare two implementations of the same function (e.g. two JSON parsers, two collection strategies)
- Track a regression across releases with a committed baseline
- Answer `kmp-expert`'s performance decision tree fallback: "profile first
  to get a concrete target"

**Trigger keywords:** benchmark, microbenchmark, kotlinx-benchmark, performance number,
measure performance, profile this, @Benchmark, JMH, is this faster, compare performance,
performance regression, benchmark baseline.

**Freshness rule:** `kotlinx-benchmark` version, the `allopen` annotation target, and
per-target registration syntax change between releases — recheck the
[kotlinx-benchmark docs](https://github.com/Kotlin/kotlinx-benchmark/tree/master/docs)
before pinning versions or copying setup code from this skill.

---

## Recommendation First

Default to a **dedicated `benchmark` source set, never commonMain or commonTest**, using
`kotlinx.benchmark.*` annotations (not `org.openjdk.jmh.annotations.*` directly) so the
same benchmark compiles across every registered target, not just JVM.

Why:
- A benchmark source set that `associateWith` the main compilation gets internal-API
  access without shipping benchmark code (or its JMH-generated subclasses) in the real
  artifact
- `kotlinx.benchmark.State`/`@Benchmark` are KMP-portable; `org.openjdk.jmh.annotations.*`
  only compiles on the JVM target — using JMH's own annotations silently locks the
  benchmark to one target
- Kotlin classes are `final` by default; JMH-style benchmarking needs the class open to
  generate subclasses — `allopen` handles this without hand-adding `open` everywhere

---

## Prerequisites

- Project scaffolded with `kmp-feature-scaffold`
- Kotlin 2.2.0+, Gradle 8+
- A concrete function/class to measure — never scaffold a benchmark module speculatively;
  see Common Anti-Patterns

---

## Step 1: Gradle plugin + allopen setup

### `libs.versions.toml`

```toml
[versions]
kotlinx-benchmark = "0.4.17"

[libraries]
kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" }

[plugins]
kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark" }
kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
```

### Module `build.gradle.kts`

```kotlin
plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlinx.benchmark)
    alias(libs.plugins.kotlin.allopen)
}

allOpen {
    // Opens classes annotated with kotlinx.benchmark.State — required because Kotlin
    // classes are final by default and the benchmark runner needs to generate
    // subclasses. Use the kotlinx.benchmark annotation, not org.openjdk.jmh's, so this
    // works for every registered target, not just JVM.
    annotation("kotlinx.benchmark.State")
}

kotlin {
    jvm()
    // Add other targets you actually benchmark — see Step 3. Don't register a target
    // here "for completeness" if nothing runs there; see Common Anti-Patterns.

    sourceSets {
        commonMain.dependencies {
            // your library code
        }
        val commonBenchmark by creating {
            dependsOn(commonMain.get())
            dependencies {
                implementation(libs.kotlinx.benchmark.runtime)
            }
        }
        val jvmBenchmark by creating {
            dependsOn(commonBenchmark)
        }
    }
}

benchmark {
    targets {
        register("jvmBenchmark") // matches the target name; registers its compilation
    }
    configurations {
        named("main") {
            warmups = 10
            iterations = 5
            iterationTime = 1
            iterationTimeUnit = "s"
        }
    }
}
```

---

## Step 2: Writing a benchmark

Place benchmark files in `src/commonBenchmark/kotlin/` (or the platform-specific
benchmark source set if the code under test is platform-specific).

```kotlin
// src/commonBenchmark/kotlin/GROUP_ID/JsonParseBenchmark.kt
package GROUP_ID

import kotlinx.benchmark.*

@State(Scope.Benchmark)
class JsonParseBenchmark {
    private lateinit var payload: String

    @Setup
    fun setup() {
        // Build fixture data ONCE per benchmark run, not per-iteration — @Setup runs
        // before the timed loop, so this cost is never measured.
        payload = buildLargeJsonFixture()
    }

    @Benchmark
    fun parseWithKotlinxSerialization(): User = Json.decodeFromString(payload)

    @Benchmark
    fun parseWithHandRolledParser(): User = HandRolledJsonParser.parse(payload)

    @TearDown
    fun tearDown() {
        // Release large fixtures explicitly if they'd otherwise skew GC pressure
        // across repeated iterations within the same JVM fork.
    }
}
```

**Rules:**
- `@State(Scope.Benchmark)` — one instance shared across all invocations in a benchmark
  run; use `Scope.Thread` only if the state must not be shared across benchmark threads
- `@Setup`/`@TearDown` — fixture cost never counts toward the measured result
- Name benchmark functions for what they measure (`parseWithKotlinxSerialization`), not
  generic names (`test1`, `benchmarkA`) — the report is only useful if the function name
  says what was measured

---

## Step 3: Multi-target registration

Register one `benchmark { targets { register(...) } }` entry per Kotlin target you
actually measure — the registered name must match an existing compilation:

```kotlin
kotlin {
    jvm()
    linuxX64()
    js(IR) { nodejs() }

    sourceSets {
        val commonBenchmark by creating { dependsOn(commonMain.get()) }
        val jvmBenchmark by creating { dependsOn(commonBenchmark) }
        val linuxX64Benchmark by creating { dependsOn(commonBenchmark) }
        val jsBenchmark by creating { dependsOn(commonBenchmark) }
    }
}

benchmark {
    targets {
        register("jvmBenchmark")
        register("linuxX64Benchmark")
        register("jsBenchmark")
    }
}
```

Native and JS results are **not directly comparable to JVM** — different runtimes, no
JIT warmup on Native, different GC (or none). Compare JVM-to-JVM and Native-to-Native;
use cross-target numbers only to confirm a target-specific regression, not to rank
targets against each other.

---

## Step 4: Running and reading results

```bash
./gradlew jvmBenchmarkBenchmark      # one target
./gradlew benchmark                  # all registered targets
```

Default output is JSON (JMH-compatible format) under
`build/reports/benchmarks/main/`. Configure `reportFormat = "csv"` in the
`configurations { named("main") { ... } }` block if you want CSV instead.

**Where results go in docs — do not paste raw output into a task note.** Per
`kmp-project-docs-maintainer`'s existing rule: write the canonical
comparison table in `docs/reference/benchmark-matrix.md` (or the nearest durable
`docs/reference/` page), and keep any task-note summary short with a link back. Commit
the benchmark source alongside the result so a future run can reproduce the number.

---

## Common Anti-Patterns

- Using `org.openjdk.jmh.annotations.*` directly instead of `kotlinx.benchmark.*` — compiles on JVM only, silently breaks Native/JS targets
- Putting benchmark code in `commonMain` or `commonTest` — ships benchmark code (and its JMH-generated subclasses on JVM) in the real artifact; always a dedicated benchmark source set
- Measuring fixture setup inside `@Benchmark` instead of `@Setup` — the timed result includes work that isn't the thing being measured
- Registering every possible target "for completeness" with nothing to actually measure there — extra compile time and CI cost for numbers nobody reads
- Comparing a JVM benchmark result directly against a Native or JS result — different runtimes; only comparable within the same target
- Pasting raw benchmark JSON/CSV into a task note instead of the canonical `docs/reference/benchmark-matrix.md` table
- Running a benchmark once and treating the number as final — rerun with the configured warmup/iteration counts; a single run is noise, not a result
- Scaffolding a benchmark module before there's a concrete function/claim to measure — benchmark the actual bottleneck found via profiling or a real performance complaint, not speculatively

---

## Testing

`kotlinx-benchmark` output is itself the test signal — there's no separate unit-test
layer for a benchmark module. Validate the setup, not the numbers:

- `./gradlew :module:jvmBenchmarkBenchmark` completes without a compile error and
  produces a JSON report under `build/reports/benchmarks/`
- Each `@Benchmark` function's name states what it measures
- `@Setup` fixture cost is excluded from the reported number (spot-check by comparing
  two runs with and without fixture-building moved into `@Benchmark` temporarily)
- The `docs/reference/benchmark-matrix.md` table is updated in the same change that adds
  or updates a benchmark

---

## Output Style

When asked to benchmark something, respond in this order:
1. Confirm there's a concrete function/claim to measure — ask if not (see Anti-Patterns)
2. Gradle setup (plugin, allopen, source set) — only the parts not already present
3. The `@Benchmark` class for the specific comparison requested
4. The run command and where the report lands
5. Where the result belongs in docs (`docs/reference/benchmark-matrix.md`)

---

## Related Skills

- `kmp-expert` — the "Improve the performance of X" decision tree routes
  here when X is unnamed or needs a concrete number before further routing
- `kmp-project-docs-maintainer` — owns the `docs/reference/benchmark-matrix.md`
  placement rule this skill's output follows
- `kmp-jni-pro` — JNI boundary-crossing cost claims should be backed by
  a benchmark from this skill, not an assumption
- `kmp-code-quality` — Detekt's complexity rules flag *style* smells;
  this skill answers whether a change is actually *faster*, a different question

---

## Changelog

| Date | Change |
|---|---|
| 2026-07-10 | Initial release — kotlinx-benchmark Gradle/allopen setup, dedicated benchmark source set convention, @State/@Benchmark/@Setup/@TearDown usage, multi-target registration, JVM-vs-Native-vs-JS comparability caveat, docs/reference/benchmark-matrix.md placement wiring. Closes the "profile first" dead-end in kmp-expert's performance decision tree. |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.