Program yang boros memori tidak selalu lambat di detik pertama — biayanya muncul pelan: garbage collector makin sering bekerja, latensi makin bergelombang, hingga OOM di jam sibuk. Dua profil pprof menjawab sisi memori: heap memotret objek yang masih hidup, allocs menjumlahkan seluruh alokasi sejak program mulai. Keduanya saling melengkapi karena masalah yang berbeda: kebocoran (banyak objek hidup) lawan pemborosan (banyak objek sekejap). Pada chapter ini kita merekam keduanya dari satu program, membaca statistik runtime.MemStats sebagai angka acuan, lalu membedah sumber alokasi dengan go tool pprof.
Counter Acuan dari runtime.MemStats #
Sebelum masuk ke profil, runtime.ReadMemStats memberi angka ringkas langsung dari runtime. Struktur runtime.MemStats memuat puluhan field; tiga yang paling penting untuk diagnosis adalah TotalAlloc (total byte yang pernah dialokasikan, kumulatif), Mallocs (jumlah operasi alokasi), dan HeapAlloc (byte yang sedang hidup). Berkas contoh.go di bawah halaman membangun laporan 100 ribu baris, mengukur selisih sebelum-sesudah, lalu menulis kedua profil:
package main
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"strings"
)
// buatLaporan membangun laporan transaksi sebagai satu string panjang.
func buatLaporan(jumlahBaris int) string {
var b strings.Builder
for i := range jumlahBaris {
fmt.Fprintf(&b, "trx-%06d status=sukses jumlah=%d\n", i, 1000+i%7)
}
return b.String()
}
func main() {
var awal, akhir runtime.MemStats
runtime.ReadMemStats(&awal)
laporan := buatLaporan(100_000)
runtime.GC() // paksakan GC sebelum statistik akhir dibaca
runtime.ReadMemStats(&akhir)
berkasHeap, err := os.CreateTemp("", "heap-*.prof")
if err != nil {
fmt.Println("gagal membuat berkas profil heap:", err)
return
}
if err := pprof.WriteHeapProfile(berkasHeap); err != nil {
fmt.Println("gagal menulis profil heap:", err)
return
}
berkasHeap.Close()
berkasAllocs, err := os.CreateTemp("", "allocs-*.prof")
if err != nil {
fmt.Println("gagal membuat berkas profil allocs:", err)
return
}
if err := pprof.Lookup("allocs").WriteTo(berkasAllocs, 0); err != nil {
fmt.Println("gagal menulis profil allocs:", err)
return
}
berkasAllocs.Close()
// Nilai mentah TotalAlloc bergeser beberapa ratus byte antar eksekusi
// karena alokasi internal runtime; pembulatan MiB dan ribuan menjadikan
// output program ini stabil untuk dibandingkan.
// Cuplikan menjaga isi laporan tetap hidup sehingga terlihat di profil heap.
fmt.Println("panjang laporan:", len(laporan), "byte")
fmt.Printf("cuplikan laporan: %q\n", laporan[:24])
fmt.Println("TotalAlloc bertambah:", (akhir.TotalAlloc-awal.TotalAlloc)/1024/1024, "MiB")
fmt.Println("Mallocs bertambah:", (akhir.Mallocs-awal.Mallocs)/1000, "ribu alokasi")
fmt.Println("profil heap ditulis: true")
fmt.Println("profil allocs ditulis: true")
}Output yang Dihasilkan:
panjang laporan: 3700000 byte
cuplikan laporan: "trx-000000 status=sukses"
TotalAlloc bertambah: 21 MiB
Mallocs bertambah: 199 ribu alokasi
profil heap ditulis: true
profil allocs ditulis: trueAngka-angka itu sudah bercerita: laporan jadi hanya 3,7 juta byte, tetapi runtime mengalokasikan 21 MiB — hampir enam kali lipatnya — dan 199 ribu operasi alokasi. Pembulatan ke MiB dan ribuan sengaja dipakai karena nilai byte persisnya bergeser beberapa ratus byte antar eksekusi (alokasi internal runtime), sedangkan angka yang dibulatkan stabil berhari-hari dan antar mesin; pola yang sama berlaku saat Anda membandingkan dua versi program.
Mari kita telaah baris demi baris program di atas:
- Baris 21–22:
ReadMemStatsmembekukan dunia sejenak untuk mengambil statistik konsisten; pembacaan pertama menjadi titik nol pengukuran. - Baris 24–26: beban kerja dijalankan, lalu
runtime.GC()memaksa pembersihan sebelum statistik akhir — tanpa ini, objek sampah masih tercatat hidup di heap profile. - Baris 33 (
WriteHeapProfile): menulis profil heap ke berkas; secara internal sama denganpprof.Lookup("heap").WriteTo. - Baris 44 (
Lookup("allocs").WriteTo): profil allocs memakai data yang sama dengan heap, hanya berbeda indeks default saat dibaca — inuse lawan alloc. - Baris 55 (
laporan[:24]): cuplikan menjaga isi string tetap hidup (terpakai di output) sehingga muncul di profil heap — tanpa pemakaian itu, GC boleh memungutnya.
Membaca Profil Heap (Objek Hidup) #
Ganti nama berkas sesuai keluaran di mesin Anda, lalu tampilkan peringkat alokasi yang masih hidup:
go tool pprof -top /tmp/heap-XXXX.profOutput yang Dihasilkan:
File: contoh
Build ID: 2019ec3b03dc40fa7671d8446e312fab72344e5e
Type: inuse_space
Time: 2026-10-13 10:55:58 UTC
Showing nodes accounting for 4729.17kB, 100% of 4729.17kB total
Showing top 6 nodes out of 9
flat flat% sum% cum cum%
4217.12kB 89.17% 89.17% 4217.12kB 89.17% strings.(*Builder).Write
512.05kB 10.83% 100% 512.05kB 10.83% runtime.mallocgc
0 0% 100% 4217.12kB 89.17% fmt.Fprintf
0 0% 100% 4217.12kB 89.17% main.buatLaporan
0 0% 100% 4217.12kB 89.17% main.main
0 0% 100% 512.05kB 10.83% runtime.(*scavengerState).initSetelah runtime.GC(), satu-satunya alokasi program yang masih hidup adalah buffer strings.Builder sebesar 4,2 MB — persis wadah yang menampung laporan 3,7 MB — ditambah sekitar 512 kB struktur internal runtime. Stack fmt.Fprintf → buatLaporan → main tampil dengan cum yang sama karena ketiganya berada di jalur pemanggilan menuju satu alokasi besar itu. Jika profil heap aplikasi production terus membesar antar dua snapshot yang jauh waktunya, itulah pola klasik kebocoran memori.
Membaca Profil Allocs (Total Alokasi) #
Profil heap menjawab “siapa yang menahan memori”, sementara profil allocs menjawab “siapa yang paling rajin mengalokasikan”. Aktifkan indeks alloc_space secara eksplisit:
go tool pprof -top -sample_index=alloc_space /tmp/allocs-XXXX.profOutput yang Dihasilkan:
File: contoh
Build ID: 2019ec3b03dc40fa7671d8446e312fab72344e5e
Type: alloc_space
Time: 2026-10-13 10:55:58 UTC
Showing nodes accounting for 23574.75kB, 100% of 23574.75kB total
Showing top 6 nodes out of 9
flat flat% sum% cum cum%
21014.67kB 89.14% 89.14% 21014.67kB 89.14% strings.(*Builder).Write
2048.03kB 8.69% 97.83% 23062.70kB 97.83% main.buatLaporan
512.05kB 2.17% 100% 512.05kB 2.17% runtime.mallocgc
0 0% 100% 21014.67kB 89.14% fmt.Fprintf
0 0% 100% 23062.70kB 97.83% main.main
0 0% 100% 512.05kB 2.17% runtime.(*scavengerState).initTotal alokasi 23,5 MB menjelaskan selisih enam kali lipat tadi: strings.Builder menumbuhkan buffer dengan menggandakan kapasitas, dan setiap pertumbuhan mengalokasikan blok baru sambil menyalin isi lama — 21 MB dari total datang dari pertumbuhan itu. Perbaikannya sudah jelas dari profil: alokasikan kapasitas awal b.Grow(...) sesuai perkiraan, atau tulis per baris ke bufio.Writer agar tidak ada penyalinan sama sekali. Bab Efisiensi Slice dan Map
membahas pola pertumbuhan ini lebih jauh.
Setelah CPU dan memori, satu dimensi diagnosis masih tersembunyi: bagaimana penjadwal membagi waktu antar goroutine. Execution tracer membuka dimensi itu pada chapter berikutnya.
Referensi #
- Package runtime/pprof — dokumentasi resmi WriteHeapProfile dan Lookup.
- A Guide to the Go Garbage Collector — cara GC mengelola heap dan konsekuensinya bagi performa.
- Profiling Go Programs with pprof — alur kerja resmi analisis profil.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"strings"
)
// buatLaporan membangun laporan transaksi sebagai satu string panjang.
func buatLaporan(jumlahBaris int) string {
var b strings.Builder
for i := range jumlahBaris {
fmt.Fprintf(&b, "trx-%06d status=sukses jumlah=%d\n", i, 1000+i%7)
}
return b.String()
}
func main() {
var awal, akhir runtime.MemStats
runtime.ReadMemStats(&awal)
laporan := buatLaporan(100_000)
runtime.GC() // paksakan GC sebelum statistik akhir dibaca
runtime.ReadMemStats(&akhir)
berkasHeap, err := os.CreateTemp("", "heap-*.prof")
if err != nil {
fmt.Println("gagal membuat berkas profil heap:", err)
return
}
if err := pprof.WriteHeapProfile(berkasHeap); err != nil {
fmt.Println("gagal menulis profil heap:", err)
return
}
berkasHeap.Close()
berkasAllocs, err := os.CreateTemp("", "allocs-*.prof")
if err != nil {
fmt.Println("gagal membuat berkas profil allocs:", err)
return
}
if err := pprof.Lookup("allocs").WriteTo(berkasAllocs, 0); err != nil {
fmt.Println("gagal menulis profil allocs:", err)
return
}
berkasAllocs.Close()
// Nilai mentah TotalAlloc bergeser beberapa ratus byte antar eksekusi
// karena alokasi internal runtime; pembulatan MiB dan ribuan menjadikan
// output program ini stabil untuk dibandingkan.
// Cuplikan menjaga isi laporan tetap hidup sehingga terlihat di profil heap.
fmt.Println("panjang laporan:", len(laporan), "byte")
fmt.Printf("cuplikan laporan: %q\n", laporan[:24])
fmt.Println("TotalAlloc bertambah:", (akhir.TotalAlloc-awal.TotalAlloc)/1024/1024, "MiB")
fmt.Println("Mallocs bertambah:", (akhir.Mallocs-awal.Mallocs)/1000, "ribu alokasi")
fmt.Println("profil heap ditulis: true")
fmt.Println("profil allocs ditulis: true")
}