Performa adalah fitur — tetapi tanpa pengujian otomatis, perubahan kode yang tampak aman bisa menurunkan throughput 20% tanpa disadari hingga produksi melambat. Performance regression testing menambal celah ini dengan cara menjalankan benchmark di setiap commit, membandingkan hasil dengan baseline, dan menandai penurunan signifikan sebagai warning atau failure pada CI. Pada chapter ini kita akan membangun benchmark runner ringan berbasis step counting (deterministik), menghasilkan output JSON yang dapat disimpan ke timeseries DB, serta mendefinisikan threshold warning dan failure.

Mengapa Step Counting, Bukan Wall-Clock? #

Benchmark wall-clock (System.nanoTime()) rentan noise: load mesin, frekuensi CPU, dan status JIT sangat bervariasi antar run. Untuk CI yang harus deterministik, pendekatan yang lebih stabil adalah menghitung langkah algoritma (jumlah comparison, jumlah swap, jumlah iterasi) — angka ini identik untuk input yang sama, terlepas dari mesin.

PendekatanDeterministikMengukurCocok untuk
Wall-clock (nanoTime)TidakWaktu nyataBenchmark lokal
Step countingYaKompleksitas algoritmaCI regression
JMH dengan forkYa (rerata)Throughput, latencyBenchmark produksi
Allocation countingYaByte allocatedHot path

Untuk CI harian, step counting memberikan sinyal paling stabil. Bila regression terdeteksi, analisis lebih lanjut dengan JMH atau profiler.

Membangun Benchmark Runner #

Cuplikan berikut mengimplementasikan runner yang menjalankan benchmark dengan warmup, mengumpulkan statistik per iterasi, dan menghasilkan ringkasan JSON untuk CI:

kotlin
// performance_regression_testing.kt — Demo benchmark dengan step counting (deterministik)

import kotlin.random.Random

// === Target function 1: BubbleSort dengan step counter ===
data class SortStats(var comparisons: Int = 0, var swaps: Int = 0)

fun bubbleSortCounted(arr: IntArray, s: SortStats) {
    val n = arr.size
    for (i in 0 until n - 1) {
        var swapped = false
        for (j in 0 until n - i - 1) {
            s.comparisons++
            if (arr[j] > arr[j + 1]) {
                val t = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = t
                s.swaps++
                swapped = true
            }
        }
        if (!swapped) return
    }
}

// === Target function 2: QuickSort dengan step counter ===
fun quickSortCounted(arr: IntArray, low: Int, high: Int, s: SortStats) {
    if (low < high) {
        val p = partitionCounted(arr, low, high, s)
        quickSortCounted(arr, low, p - 1, s)
        quickSortCounted(arr, p + 1, high, s)
    }
}

fun partitionCounted(arr: IntArray, low: Int, high: Int, s: SortStats): Int {
    val pivot = arr[high]
    var i = low - 1
    for (j in low until high) {
        s.comparisons++
        if (arr[j] <= pivot) {
            i++
            if (i != j) {
                val t = arr[i]; arr[i] = arr[j]; arr[j] = t
                s.swaps++
            }
        }
    }
    val t = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = t
    s.swaps++
    return i + 1
}

// === Target function 3: stdlib sortedArray (step = 1, dianggap black box) ===
fun stdlibSortCounted(arr: IntArray): IntArray = arr.sortedArray()

// === Benchmark runner: hitung step rata-rata, deterministic ===
data class BenchResult(
    val name: String,
    val iterations: Int,
    val totalComparisons: Long,
    val totalSwaps: Long,
    val meanComparisons: Double,
    val meanSwaps: Double
) {
    fun toRow(): String =
        "%-22s iter=%-4d meanCmp=%-9.1f meanSwap=%-9.1f".format(
            name, iterations, meanComparisons, meanSwaps
        )
}

fun benchmark(
    name: String,
    iterations: Int,
    seedBase: Int,
    input: (Int, Int) -> IntArray,
    action: (IntArray, SortStats) -> Unit
): BenchResult {
    val warmup = iterations / 5
    repeat(warmup) {
        val arr = input(seedBase + it, 200)
        action(arr, SortStats())
    }
    var totalCmp = 0L
    var totalSw = 0L
    val measured = iterations - warmup
    for (i in 0 until measured) {
        val arr = input(seedBase + warmup + i, 200)
        val s = SortStats()
        action(arr, s)
        totalCmp += s.comparisons
        totalSw += s.swaps
    }
    return BenchResult(
        name = name,
        iterations = measured,
        totalComparisons = totalCmp,
        totalSwaps = totalSw,
        meanComparisons = totalCmp.toDouble() / measured,
        meanSwaps = totalSw.toDouble() / measured
    )
}

fun makeInput(seed: Int, size: Int): IntArray {
    val rnd = Random(seed.toLong())
    return IntArray(size) { rnd.nextInt(0, 1000) }
}

fun main() {
    println("=== Performance Regression Testing ===")
    println()
    println("Benchmark dengan step counting (deterministik) untuk CI tracking.")
    println()

    val size = 200
    val iterations = 200
    val seedBase = 1000

    println("=== Konfigurasi ===")
    println("  Array size: $size elemen acak")
    println("  Iterasi per benchmark: $iterations (160 setelah warmup)")
    println("  Seed base: $seedBase (input identik antar run)")
    println()

    val results = listOf(
        benchmark("bubbleSort", iterations, seedBase, ::makeInput) { arr, s -> bubbleSortCounted(arr, s) },
        benchmark("quickSort", iterations, seedBase, ::makeInput) { arr, s -> quickSortCounted(arr, 0, arr.size - 1, s) }
    )

    println("=== Hasil Benchmark ===")
    results.forEach { println("  " + it.toRow()) }
    println()

    // === Deteksi regression ===
    println("=== Deteksi Regression ===")
    val baselineMean = results[0].meanComparisons
    results.forEach { r ->
        val ratio = baselineMean / r.meanComparisons
        val status = if (ratio > 1.0) "lebih lambat dari baseline" else "lebih cepat dari baseline"
        println("  %-22s rasio=%.2fx %s".format(r.name, ratio, status))
    }
    println()

    // === Format output JSON (untuk CI) ===
    println("=== Output JSON (untuk CI tracking) ===")
    println("{")
    println("  \"benchmark\": \"sort_step_counting\",")
    println("  \"size\": $size,")
    println("  \"results\": [")
    results.forEachIndexed { idx, r ->
        val trailing = if (idx < results.size - 1) "," else ""
        println("    {\"name\": \"${r.name}\", \"meanComparisons\": ${"%.1f".format(r.meanComparisons)}, \"iterations\": ${r.iterations}}$trailing")
    }
    println("  ]")
    println("}")
    println()

    // === Threshold regression ===
    println("=== Threshold Regression ===")
    println("  CI membandingkan meanComparisons dengan baseline pada commit sebelumnya")
    println("  Bila mean baru > 1.10x baseline -> WARNING (>10% regression)")
    println("  Bila mean baru > 1.25x baseline -> FAIL (>25% regression)")
    println()

    // === Aturan praktis ===
    println("=== Aturan Praktis ===")
    val rules = listOf(
        "1. Selalu lakukan warmup sebelum pengukuran (JIT perlu waktu)",
        "2. Pakai seed acak yang sama antar run (Random(seed))",
        "3. Bandingkan mean + percentile, bukan hanya min/max",
        "4. Track hasil di CI dengan JSON output ke timeseries DB",
        "5. Set threshold warning 10%, fail 25% dari baseline",
        "6. Jangan benchmark di mesin noise; pakai dedicated runner",
        "7. Untuk output deterministik, hitung langkah (step) bukan waktu"
    )
    rules.forEach { println("  " + it) }
}

Output yang Dihasilkan:

text
=== Performance Regression Testing ===

Benchmark dengan step counting (deterministik) untuk CI tracking.

=== Konfigurasi ===
  Array size: 200 elemen acak
  Iterasi per benchmark: 200 (160 setelah warmup)
  Seed base: 1000 (input identik antar run)

=== Hasil Benchmark ===
  bubbleSort             iter=160  meanCmp=19749.0   meanSwap=9987.5   
  quickSort              iter=160  meanCmp=1556.9    meanSwap=715.3    

=== Deteksi Regression ===
  bubbleSort             rasio=1.00x lebih cepat dari baseline
  quickSort              rasio=12.68x lebih lambat dari baseline

=== Output JSON (untuk CI tracking) ===
{
  "benchmark": "sort_step_counting",
  "size": 200,
  "results": [
    {"name": "bubbleSort", "meanComparisons": 19749.0, "iterations": 160},
    {"name": "quickSort", "meanComparisons": 1556.9, "iterations": 160}
  ]
}

=== Threshold Regression ===
  CI membandingkan meanComparisons dengan baseline pada commit sebelumnya
  Bila mean baru > 1.10x baseline -> WARNING (>10% regression)
  Bila mean baru > 1.25x baseline -> FAIL (>25% regression)

=== Aturan Praktis ===
  1. Selalu lakukan warmup sebelum pengukuran (JIT perlu waktu)
  2. Pakai seed acak yang sama antar run (Random(seed))
  3. Bandingkan mean + percentile, bukan hanya min/max
  4. Track hasil di CI dengan JSON output ke timeseries DB
  5. Set threshold warning 10%, fail 25% dari baseline
  6. Jangan benchmark di mesin noise; pakai dedicated runner
  7. Untuk output deterministik, hitung langkah (step) bukan waktu

Mari kita bedah hasil di atas:

  1. Konfigurasi: setiap run memakai seedBase = 1000 sehingga input identik antar commit. Warmup 40 iterasi (20%) memastikan JIT sudah mengoptimalkan path sebelum pengukuran dimulai.
  2. Hasil Benchmark: bubbleSort rata-rata 19749 comparison dan 9987 swap per iterasi. quickSort hanya 1557 comparison dan 715 swap — sesuai teori O(n²) vs O(n log n).
  3. Deteksi Regression: baseline adalah bubbleSort. Rasio quickSort / bubbleSort ≈ 0.08 — artinya quickSort 12.68x lebih cepat dari bubbleSort. Bila di commit berikutnya quickSort tiba-tiba menjadi rasio 6x (bukan 12x), CI menandai regression karena complexity naik 2x.
  4. Output JSON: format ini mudah disimpan ke timeseries DB (mis. InfluxDB, Prometheus, atau simple JSON file di git). CI membandingkan nilai meanComparisons dengan baseline commit sebelumnya.
  5. Threshold: 10% regression → warning (PR tetap boleh merge dengan review). 25% regression → fail (PR ditolak otomatis). Threshold harus disesuaikan per benchmark — sort dengan 10% regression berbeda artinya dengan network call 10% regression.

Integrasi CI dengan JMH #

Untuk benchmark produksi, JMH (Java Microbenchmark Harness) adalah standar industri. JMH mengelola warmup, fork, dan statistical analysis (percentile, confidence interval). Output JMH dalam format JSON bisa di-parse oleh CI:

bash JMH dengan output JSON
java -jar benchmarks.jar \
  -wi 5 -i 10 -f 1 \
  -rf json -rff benchmark-results.json

CI kemudian membandingkan benchmark-results.json dengan baseline menggunakan tool seperti jmh-baseline-compare atau script Python sederhana.

Tracking Performance over Time #

Output benchmark yang disimpan ke timeseries DB memungkinkan visualisasi tren performance. Dashboard Grafana bisa menampilkan plot meanComparisons vs commit, sehingga regression gradual (mis. 2% per minggu) terlihat sebelum mengakibatkan failure.

Sumber dataTool populerInterval simpan
JSON di artifactGitHub Actions artifact90 hari
Timeseries DBInfluxDB + Grafana1-2 tahun
CI metricsDatadog, NewRelictergantung plan
Git LFS benchmarkbenchmark.json di reposelamanya

Referensi Lanjutan #

Selanjutnya #

Performance regression testing menutup loop pengembangan performance — dari pengukuran hingga deteksi otomatis. Bab-bab berikutnya bergeser ke deployment: bagaimana memaketkan aplikasi Kotlin menjadi artefak siap produksi. Materi tersebut dimulai pada chapter Packaging JVM Applications.