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 profiler | Yang diukur | Tools |
|---|---|---|
| CPU sampler | Panggilan function, hot spots | VisualVM, async-profiler |
| Memory profiler | Alokasi objek, GC pressure | VisualVM, JFR |
| Allocation profiler | Lokasi alokasi spesifik | async-profiler, JFR |
| Thread profiler | Wait/block state | JFR, 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.
| Aspek | CPU Sampler | Memory Profiler |
|---|---|---|
| Yang diukur | Function call frequency | Object allocation |
| Overhead | Rendah (1-5%) | Tinggi (10-30%) |
| Cocok untuk | Menemukan hot CPU | Menemukan GC pressure |
| Format output | Flamegraph, top-N functions | Histogram 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:
// 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:
=== 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 5Mari kita bedah bagian-bagian kunci dari kode di atas:
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.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.recordAllocation("Int", 1): setiap kaliIntdi-box (misalnya return darihitungDiskon), alokasi dicatat. Pada profiler nyata, ini terlihat sebagai histogram “java.lang.Integer”.- Sorting
functionHits.entries.sortedByDescending { it.value }: profiler menyajikan function dengan count tertinggi terlebih dahulu. Inilah yang membuat hotspot mudah dikenali. - 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.
./profiler.sh -d 30 -f /tmp/flame.html $(pgrep -f app.jar)
# Hasil: flamegraph HTML yang bisa dibuka di browserSetelah 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.