JMH (Java Microbenchmark Harness) adalah framework benchmark yang dikembangkan oleh tim OpenJDK. JMH menangani semua jebakan microbenchmark yang kita bahas di chapter sebelumnya secara otomatis: warmup iteration, dead code elimination via Blackhole, forked JVM untuk isolasi, dan statistik robust (median, percentil). Pada bab ini kita akan membedah struktur class JMH, konfigurasi Gradle untuk menjalankannya, dan annotation kunci yang paling sering dipakai. Demo memakai step-counting karena JMH nyata memerlukan setup Maven/Gradle.

Struktur Class JMH #

Class JMH terdiri dari tiga bagian: @State class yang menampung data shared, method @Benchmark yang diukur, dan annotation konfigurasi (@Warmup, @Measurement). Method benchmark dianotasi @Benchmark dan dipanggil otomatis oleh runner JMH.

kotlin Struktur dasar class JMH
@State(Scope.Benchmark)
class MyState {
    val data: List<Int> = (1..1000).toList()
}

class MyBenchmark {
    @Benchmark
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 5, time = 1)
    fun sumList(state: MyState): Long {
        return state.data.sumOf { it.toLong() }
    }
}

@State(Scope.Benchmark) membuat instance state disharing ke semua thread benchmark. Scope.Thread membuat instance per-thread. Pilihan scope memengaruhi validitas benchmark untuk kode concurrent.

Annotation Kunci #

AnnotationFungsiContoh
@BenchmarkMenandai method yang diukur@Benchmark fun sum()
@StateMenandai class sebagai state shared@State class MyState
@WarmupKonfigurasi warmup@Warmup(iterations=5, time=1)
@MeasurementKonfigurasi pengukuran@Measurement(iterations=5, time=1)
@ForkJumlah JVM fork@Fork(1)
@BenchmarkModeMode pengukuran@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnitSatuan waktu output@OutputTimeUnit(TimeUnit.MICROSECONDS)

@Fork penting karena setiap fork menjalankan JVM baru, sehingga JIT state bersih. Tanpa fork, hasil benchmark dapat dipengaruhi oleh JIT yang sudah aktif dari benchmark sebelumnya.

Demo Struktur JMH #

Karena JMH nyata memerlukan plugin Gradle/Maven, cuplikan berikut mendemokan struktur class dan simulasi runner. Simulasi menjalankan warmup dan measurement phase dengan step counter:

kotlin
// Demo struktur JMH (Java Microbenchmark Harness).
// Mensimulasikan anotasi @Benchmark, @State, @Warmup, @Measurement
// dengan struktur yang menggambarkan fase-fase JMH.

annotation class Benchmark
annotation class State
annotation class Warmup(val iterations: Int, val time: Int)
annotation class Measurement(val iterations: Int, val time: Int)

@State
class MyState {
    val data: List<Int> = (1..1000).toList()
}

class MyBenchmark {
    @Benchmark
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 5, time = 1)
    fun sumList(state: MyState): Long {
        return state.data.sumOf { it.toLong() }
    }

    @Benchmark
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 5, time = 1)
    fun sumSequence(state: MyState): Long {
        return state.data.asSequence().sumOf { it.toLong() }
    }
}

// Simulasi runner JMH yang menjalankan benchmark dengan fase
// warmup dan measurement. Output deterministik berupa langkah.

class JmhSimulator {
    var totalOps: Long = 0
    val results: MutableList<Pair<String, Long>> = mutableListOf()

    fun run(name: String, warmupIter: Int, measureIter: Int, body: () -> Long) {
        print("Benchmark $name: ")
        // Warmup phase
        var warmupOps = 0L
        repeat(warmupIter) {
            warmupOps += body()
        }
        // Measurement phase
        var measureOps = 0L
        repeat(measureIter) {
            measureOps += body()
        }
        results.add(name to measureOps)
        totalOps += measureOps
        println("warmup=$warmupIter iters, measure=$measureIter iters, total ops=$measureOps")
    }

    fun report() {
        println()
        println("=== JMH Report ===")
        results.forEach { (name, ops) ->
            val bar = "#".repeat((ops * 20 / (results.maxOf { it.second } + 1)).toInt())
            println("  %-25s ops/iter=%-10d [%s]".format(name, ops / 5, bar))
        }
    }
}

fun main() {
    println("=== JMH (Java Microbenchmark Harness) ===")
    println()

    // 1. Struktur kelas benchmark
    println("=== 1. Struktur Class Benchmark ===")
    println("@State class MyState { val data = (1..1000).toList() }")
    println()
    println("class MyBenchmark {")
    println("    @Benchmark")
    println("    @Warmup(iterations = 5, time = 1)")
    println("    @Measurement(iterations = 5, time = 1)")
    println("    fun sumList(state: MyState): Long = state.data.sumOf { it.toLong() }")
    println()
    println("    @Benchmark")
    println("    @Warmup(iterations = 5, time = 1)")
    println("    @Measurement(iterations = 5, time = 1)")
    println("    fun sumSequence(state: MyState): Long =")
    println("        state.data.asSequence().sumOf { it.toLong() }")
    println("}")
    println()

    // 2. Simulasi runner JMH
    println("=== 2. Simulasi Runner JMH ===")
    val sim = JmhSimulator()
    val state = MyState()

    sim.run("sumList", warmupIter = 5, measureIter = 5) { state.data.sumOf { it.toLong() } }
    sim.run("sumSequence", warmupIter = 5, measureIter = 5) {
        state.data.asSequence().sumOf { it.toLong() }
    }

    sim.report()
    println()
    println("Catatan: simulasi pakai step counter; JMH nyata melaporkan ops/detik.")
    println()

    // 3. Build configuration
    println("=== 3. Build Configuration (Gradle) ===")
    val gradleConfig = """
        |plugins {
        |    kotlin("jvm") version "2.0.21"
        |    id("me.champeau.jmh") version "0.7.2"
        |}
        |
        |jmh {
        |    fork = 1
        |    warmupIterations = 5
        |    iterations = 5
        |    warmupForks = 0
        |    timeOnIteration = "1s"
        |    benchmarkMode.set(listOf("avgt"))
        |    timeUnit.set("us")
        |    resultFormat.set("JSON")
        |}
    """.trimMargin()
    println(gradleConfig)
    println()

    // 4. Annotation penjelasan
    println("=== 4. Annotation Kunci JMH ===")
    val annotations = listOf(
        "@Benchmark" to "Menandai method yang akan diukur",
        "@State" to "Menandai class sebagai state shared antar benchmark",
        "@Warmup" to "Konfigurasi fase warmup (iterations, time)",
        "@Measurement" to "Konfigurasi fase pengukuran (iterations, time)",
        "@Fork" to "Jumlah JVM fork untuk isolasi pengukuran",
        "@BenchmarkMode" to "Mode: avgt (average), thrpt (throughput), sample",
        "@OutputTimeUnit" to "Satuan waktu output (us, ms, s)"
    )
    annotations.forEach { (ann, desc) ->
        println("  %-25s $desc".format(ann))
    }
}

Output yang Dihasilkan:

text
=== JMH (Java Microbenchmark Harness) ===

=== 1. Struktur Class Benchmark ===
@State class MyState { val data = (1..1000).toList() }

class MyBenchmark {
    @Benchmark
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 5, time = 1)
    fun sumList(state: MyState): Long = state.data.sumOf { it.toLong() }

    @Benchmark
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 5, time = 1)
    fun sumSequence(state: MyState): Long =
        state.data.asSequence().sumOf { it.toLong() }
}

=== 2. Simulasi Runner JMH ===
Benchmark sumList: warmup=5 iters, measure=5 iters, total ops=2502500
Benchmark sumSequence: warmup=5 iters, measure=5 iters, total ops=2502500

=== JMH Report ===
  sumList                   ops/iter=500500     [###################]
  sumSequence               ops/iter=500500     [###################]

Catatan: simulasi pakai step counter; JMH nyata melaporkan ops/detik.

=== 3. Build Configuration (Gradle) ===
plugins {
    kotlin("jvm") version "2.0.21"
    id("me.champeau.jmh") version "0.7.2"
}

jmh {
    fork = 1
    warmupIterations = 5
    iterations = 5
    warmupForks = 0
    timeOnIteration = "1s"
    benchmarkMode.set(listOf("avgt"))
    timeUnit.set("us")
    resultFormat.set("JSON")
}

=== 4. Annotation Kunci JMH ===
  @Benchmark                Menandai method yang akan diukur
  @State                    Menandai class sebagai state shared antar benchmark
  @Warmup                   Konfigurasi fase warmup (iterations, time)
  @Measurement              Konfigurasi fase pengukuran (iterations, time)
  @Fork                     Jumlah JVM fork untuk isolasi pengukuran
  @BenchmarkMode            Mode: avgt (average), thrpt (throughput), sample
  @OutputTimeUnit           Satuan waktu output (us, ms, s)

Mari kita bedah bagian-bagian kunci dari kode di atas:

  1. Custom @Benchmark, @State, @Warmup, @Measurement: pada JMH nyata, annotation ini berasal dari package org.openjdk.jmh.annotations. Di sini kita buat versi dummy agar kode dapat dikompilasi tanpa dependency JMH. Pada project nyata, tambahkan jmh-core ke dependency.
  2. JmhSimulator.run: function ini menjalankan dua fase. Warmup (5 iterasi) lalu measurement (5 iterasi). Hasil measurement yang dilaporkan, bukan warmup. Pola ini sama dengan JMH nyata.
  3. State.data: data 1000 elemen yang akan diproses di setiap iterasi. Pada JMH nyata, state di-setup sekali per @Setup lalu dipakai semua iterasi benchmark.
  4. Gradle plugin me.champeau.jmh: plugin resmi untuk JMH di Gradle. Setelah konfigurasi, jalankan ./gradlew jmh dan hasil benchmark muncul di build/reports/jmh/results.json.
  5. fork = 1: setiap benchmark dijalankan di JVM fork terpisah, sehingga JIT state bersih. Untuk pengukuran serius, gunakan fork = 3 agar hasil robust terhadap variasi startup JVM.

Menjalankan JMH di Gradle #

Setelah plugin terpasang, tempatkan class benchmark di folder src/jmh/kotlin. Plugin akan otomatis mendeteksi method @Benchmark dan menjalankannya saat ./gradlew jmh.

bash Menjalankan benchmark JMH
./gradlew jmh
# Hasil: build/reports/jmh/results.json
#        build/reports/jmh/index.html

Hasil JSON dapat diproses lebih lanjut (misal dibandingkan dengan baseline untuk regression testing). Format HTML menyediakan visualisasi bar chart untuk perbandingan antar benchmark.

Benchmark Mode #

JMH mendukung beberapa mode pengukuran:

ModeYang diukurCocok untuk
AverageTimeWaktu rata-rata per operasiBenchmark CPU-bound umum
ThroughputOperasi per detikThroughput server
SampleTimeDistribusi waktu (percentile)Latensi p99
SingleShotTimeSatu kali pengukuranCold start, startup time

Pilihan mode bergantung pada pertanyaan yang ingin dijawab. Untuk “berapa cepat function A dibanding B”, AverageTime cukup. Untuk “berapa request per detik yang bisa dilayani”, Throughput lebih tepat.

Referensi Lanjutan #

  • JMH Project — dokumentasi resmi JMH untuk membuat benchmark JVM
  • JMH Source — kode sumber JMH beserta contoh benchmark dan modul jmh-core

Selanjutnya #

JMH memberi pengukuran akurat. Bab berikutnya membahas allocation profiling — teknik menemukan alokasi objek berlebih yang menyebabkan GC pressure. Materi tersebut ada di chapter Allocation Profiling.