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.
| Pendekatan | Deterministik | Mengukur | Cocok untuk |
|---|---|---|---|
Wall-clock (nanoTime) | Tidak | Waktu nyata | Benchmark lokal |
| Step counting | Ya | Kompleksitas algoritma | CI regression |
| JMH dengan fork | Ya (rerata) | Throughput, latency | Benchmark produksi |
| Allocation counting | Ya | Byte allocated | Hot 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:
// 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:
=== 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 waktuMari kita bedah hasil di atas:
- Konfigurasi: setiap run memakai
seedBase = 1000sehingga input identik antar commit. Warmup 40 iterasi (20%) memastikan JIT sudah mengoptimalkan path sebelum pengukuran dimulai. - Hasil Benchmark:
bubbleSortrata-rata 19749 comparison dan 9987 swap per iterasi.quickSorthanya 1557 comparison dan 715 swap — sesuai teori O(n²) vs O(n log n). - Deteksi Regression: baseline adalah
bubbleSort. RasioquickSort / 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. - Output JSON: format ini mudah disimpan ke timeseries DB (mis. InfluxDB, Prometheus, atau simple JSON file di git). CI membandingkan nilai
meanComparisonsdengan baseline commit sebelumnya. - 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:
java -jar benchmarks.jar \
-wi 5 -i 10 -f 1 \
-rf json -rff benchmark-results.jsonCI 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 data | Tool populer | Interval simpan |
|---|---|---|
| JSON di artifact | GitHub Actions artifact | 90 hari |
| Timeseries DB | InfluxDB + Grafana | 1-2 tahun |
| CI metrics | Datadog, NewRelic | tergantung plan |
| Git LFS benchmark | benchmark.json di repo | selamanya |
Referensi Lanjutan #
- JMH (Java Microbenchmark Harness) — dasar benchmark JMH yang umum dipakai sebagai gerbang regresi performa
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.