Aplikasi Kotlin yang lambat sering tidak menunjukkan penyebabnya dengan jelas. Kode yang terlihat sederhana bisa menjadi bottleneck karena alokasi berlebih, panggilan function berulang, atau pemborosan collection. Profiling adalah proses mengukur di mana waktu dan memori benar-benar dihabiskan, sehingga optimasi bisa tepat sasaran. Pada bab ini kita akan mengenal tools profiling utama (VisualVM, async-profiler, JFR), dua jenis profiler (CPU sampler dan memory profiler), serta teknik step-counting yang memberi output deterministik untuk demo dan pengajaran.

Mengapa Perlu Profiling #

Tanpa profiling, optimasi sering ditembak dengan intuisi: “kelihatannya loop ini lambat” atau “pasti karena I/O”. Intuisi sering kali salah. Hukum Amdahl menyatakan bahwa percepatan total dibatasi oleh bagian yang tidak dioptimasi. Bila 80% waktu dihabiskan di function A, mengoptimasi function B yang hanya menyumbang 5% tidak akan banyak membantu. Profiling memberi data konkret: function mana yang paling sering dipanggil, alokasi objek mana yang paling banyak, dan di mana thread paling banyak menunggu.

Jenis profilerYang diukurTools
CPU samplerPanggilan function, hot spotsVisualVM, async-profiler
Memory profilerAlokasi objek, GC pressureVisualVM, JFR
Allocation profilerLokasi alokasi spesifikasync-profiler, JFR
Thread profilerWait/block stateJFR, IntelliJ Profiler

Tiga Tools Utama #

VisualVM adalah profiler gratis dari Oracle yang menyediakan CPU sampler, memory profiler, dan thread monitor. Tool ini cocok untuk developer individual karena UI-nya intuitif. VisualVM terhubung ke aplikasi via JMX (Java Management Extensions) dan dapat diaktifkan saat runtime tanpa restart.

async-profiler adalah profiler low-overhead dari JetBrains yang memakai perf_event di Linux atau DTrace di macOS. Tool ini menghasilkan flamegraph yang menunjukkan tumpukan panggilan dengan persentase waktu. async-profiler cocok untuk aplikasi production karena overhead-nya minimal (1-3%).

Java Flight Recorder (JFR) adalah profiler bawaan JDK (sejak OpenJDK 11) yang merekam event aplikasi ke file .jfr. File tersebut dapat dianalisis dengan JDK Mission Control atau IntelliJ Profiler. JFR cocok untuk produksi karena recording dapat diaktifkan tanpa downtime.

CPU Sampler vs Memory Profiler #

CPU sampler mengambil snapshot stack trace secara berkala (misalnya setiap 10 ms). Function yang muncul paling sering di snapshot adalah hot spots. Memory profiler melacak setiap alokasi objek dan menyajikan tipe yang paling banyak dialokasikan.

AspekCPU SamplerMemory Profiler
Yang diukurFunction call frequencyObject allocation
OverheadRendah (1-5%)Tinggi (10-30%)
Cocok untukMenemukan hot CPUMenemukan GC pressure
Format outputFlamegraph, top-N functionsHistogram by class

Demo Profiling Step-Counting #

Karena profiler nyata memerlukan setup eksternal, cuplikan berikut mendemonstrasikan konsep profiling dengan ProfileCounter yang mencatat setiap function call dan alokasi. Output deterministik, mencerminkan apa yang dilihat pengguna di profiler nyata:

kotlin
// Demo konsep profiling: menemukan hotspot dengan step counter.
// Simulasi output profiler: function call count, allocation count,
// dan persentase waktu (deterministik, bukan nanotime).

class ProfileCounter {
    var totalSteps: Long = 0
    private val functionHits: MutableMap<String, Long> = mutableMapOf()
    private val allocationHits: MutableMap<String, Long> = mutableMapOf()

    fun recordFunction(name: String, steps: Long = 1) {
        functionHits[name] = (functionHits[name] ?: 0L) + steps
        totalSteps += steps
    }

    fun recordAllocation(type: String, count: Long = 1) {
        allocationHits[type] = (allocationHits[type] ?: 0L) + count
    }

    fun report(): String {
        val sb = StringBuilder()
        sb.appendLine("=== Profiling Report ===")
        sb.appendLine("Total steps: $totalSteps")
        sb.appendLine()
        sb.appendLine("=== Top Functions (by steps) ===")
        functionHits.entries.sortedByDescending { it.value }.take(5).forEach { (name, hits) ->
            val persen = hits * 100 / totalSteps
            val bar = "#".repeat((persen / 5).toInt().coerceAtMost(20))
            sb.appendLine("  %-25s %6d (%2d%%) [%s]".format(name, hits, persen, bar))
        }
        sb.appendLine()
        sb.appendLine("=== Top Allocations ===")
        allocationHits.entries.sortedByDescending { it.value }.forEach { (type, count) ->
            sb.appendLine("  %-25s x %d".format(type, count))
        }
        return sb.toString()
    }
}

data class Pesanan(val id: Int, val total: Int, val items: List<String>)

fun prosesPesanan(pesanan: Pesanan, counter: ProfileCounter) {
    counter.recordFunction("prosesPesanan", 1)
    val diskon = hitungDiskon(pesanan.total, counter)
    val pajak = hitungPajak(pesanan.total, counter)
    val total = pesanan.total - diskon + pajak
    counter.recordAllocation("String (ringkasan)", 1)
    println("  Pesanan #${pesanan.id}: total=$total")
}

fun hitungDiskon(total: Int, counter: ProfileCounter): Int {
    counter.recordFunction("hitungDiskon", 1)
    counter.recordAllocation("Int", 1)
    return if (total > 100_000) total / 10 else 0
}

fun hitungPajak(total: Int, counter: ProfileCounter): Int {
    counter.recordFunction("hitungPajak", 1)
    counter.recordAllocation("Int", 1)
    return total * 11 / 100
}

fun main() {
    println("=== Profiling Kotlin Applications ===")
    println()

    val counter = ProfileCounter()
    val pesananList = listOf(
        Pesanan(1, 50_000, listOf("buku")),
        Pesanan(2, 150_000, listOf("laptop", "mouse")),
        Pesanan(3, 250_000, listOf("monitor")),
        Pesanan(4, 75_000, listOf("keyboard")),
        Pesanan(5, 500_000, listOf("handphone"))
    )

    println("Memproses ${pesananList.size} pesanan...")
    pesananList.forEach { prosesPesanan(it, counter) }
    println()

    print(counter.report())
}

Output yang Dihasilkan:

text
=== Profiling Kotlin Applications ===

Memproses 5 pesanan...
  Pesanan #1: total=55500
  Pesanan #2: total=151500
  Pesanan #3: total=252500
  Pesanan #4: total=83250
  Pesanan #5: total=505000

=== Profiling Report ===
Total steps: 15

=== Top Functions (by steps) ===
  prosesPesanan                  5 (33%) [######]
  hitungDiskon                   5 (33%) [######]
  hitungPajak                    5 (33%) [######]

=== Top Allocations ===
  Int                       x 10
  String (ringkasan)        x 5

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

  1. ProfileCounter: class yang merekam function call dan alokasi ke map terpisah. Di profiler nyata, recording ini dilakukan otomatis oleh JVM agent. Di sini kita melakukannya manual untuk keperluan demo.
  2. recordFunction("hitungDiskon", 1): setiap function yang dipanggil mencatat dirinya ke counter. Pada profiler nyata, ini setara dengan sample stack trace yang menemukan function paling sering muncul.
  3. recordAllocation("Int", 1): setiap kali Int di-box (misalnya return dari hitungDiskon), alokasi dicatat. Pada profiler nyata, ini terlihat sebagai histogram “java.lang.Integer”.
  4. Sorting functionHits.entries.sortedByDescending { it.value }: profiler menyajikan function dengan count tertinggi terlebih dahulu. Inilah yang membuat hotspot mudah dikenali.
  5. Visualisasi bar #.repeat(persen/5)`: profiler nyata menyajikan flamegraph atau tree map. Untuk demo deterministik, bar ASCII sederhana sudah cukup menggambarkan distribusi waktu.

Cara Memulai Profiling #

Langkah pertama profiling adalah menentukan apa yang diukur. Untuk CPU, jalankan async-profiler dengan ./profiler.sh -d 30 -f flame.html <pid>. Untuk memory, gunakan VisualVM dan ambil heap dump. Untuk thread, aktifkan JFR recording dengan jcmd <pid> JFR.start duration=60s filename=app.jfr.

bash Memulai async-profiler (Linux)
./profiler.sh -d 30 -f /tmp/flame.html $(pgrep -f app.jar)
# Hasil: flamegraph HTML yang bisa dibuka di browser

Setelah data terkumpul, cari dua hal: function dengan panggilan terbanyak (mungkin perlu caching) dan function dengan alokasi terbanyak (mungkin perlu reuse object). Hanya optimasi yang didukung data profiler yang layak diimplementasi.

Referensi Lanjutan #

  • JetBrains Profiler — panduan resmi IntelliJ Profiler untuk memprofil aplikasi Kotlin/JVM dan menganalisis penggunaan CPU, memori, serta thread

Selanjutnya #

Profiling menunjukkan di mana waktu dihabiskan. Untuk pengukuran kuantitatif yang akurat, kita perlu benchmark. Bab berikutnya membahas fundamental benchmarking: jebakan umum seperti JIT warmup, dead code elimination, dan GC pauses. Materi tersebut ada di chapter Benchmarking Fundamentals.