Pelacakan dan kebocoran

Tracking_Allocator dan pelaporan kebocoran memori dalam praktik.

Daftar isi

Kebocoran memori pada program tanpa pengumpul sampah hanya dapat ditemukan dengan mencatat setiap alokasi, dan paket core:mem menyediakan alatnya dalam bentuk Tracking_Allocator. Alokator pelacak membungkus alokator lain, mencatat setiap permintaan ke dalam map, dan menyisakan laporan berupa entri yang belum dilepaskan ketika program hampir berakhir. Topik ini membahas cara kerjanya, program yang sengaja membiarkan satu alokasi bocor beserta laporannya, versi yang sudah diperbaiki, dan cara memasang pola ini pada program nyata.

Cara Tracking_Allocator membungkus alokator#

Tracking_Allocator adalah struct yang menyimpan alokator penopang dan map bernama allocation_map yang berkunci pointer hasil alokasi. Setiap permintaan diteruskan ke alokator penopang, lalu entri dicatat berisi ukuran, alignment, mode permintaan, error, dan lokasi sumber berupa berkas serta baris pemanggil. Field location bertipe Source_Code_Location inilah yang membuat laporan kebocoran menunjuk ke baris make yang bersalah. Statistik agregat juga ikut tercatat, antara lain total_memory_allocated, total_allocation_count, total_memory_freed, total_free_count, dan current_memory_allocated. Pemasangannya mengikuti pola empat langkah yang selalu sama.

  1. Deklarasikan ta: mem.Tracking_Allocator di awal proc yang dipantau.
  2. Pasangkan dengan alokator penopang melalui mem.tracking_allocator_init(&ta, context.allocator).
  3. Jadikan alokator aktif melalui context.allocator = mem.tracking_allocator(&ta) di dalam lingkup yang disimpan dan dipulihkan seperti pada topik sistem context.
  4. Di akhir program, iterasi ta.allocation_map untuk melaporkan entri yang tersisa.

Satu perbedaan perilaku perlu diwaspadai: pelepasan pointer yang tidak pernah dialokasikan dari pelacak ini memanggil callback panic bawaan, sehingga free yang keliru langsung terdeteksi saat program berjalan. Kebutuhan internal pelacak, yaitu memori untuk map-nya sendiri, dialokasikan dari alokator yang dipasang saat inisialisasi dan tidak ikut tercatat dalam map.

Program yang sengaja bocor#

Program berikut mengikuti pola empat langkah itu, dengan satu alokasi yang sengaja dibiarkan tanpa delete dan satu alokasi yang dilepaskan melalui defer. Laporan di akhir program hanya menampilkan entri yang tersisa, sehingga kebocoran yang benar-benar ada menjadi mudah dibaca.

odin laporan-kebocoran.odin
package main

import "core:fmt"
import "core:mem"
import "core:strings"
import "base:runtime"

// proc ini sengaja membiarkan satu alokasi tidak dilepaskan
proses_pengukuran :: proc() {
        // alokasi ini bocor: tidak ada delete yang menyertainya
        suhu := make([]f32, 3)
        suhu[0], suhu[1], suhu[2] = 28.4, 29.1, 30.2

        // alokasi ini dilepaskan lewat defer
        cadangan := make([]f32, 2)
        defer delete(cadangan)
        cadangan[0], cadangan[1] = 28.4, 29.1
}

// potong jalur berkas sehingga laporan hanya menampilkan nama berkasnya
nama_berkas :: proc(lokasi: runtime.Source_Code_Location) -> string {
        if i := strings.last_index_byte(lokasi.file_path, '/'); i >= 0 {
                return lokasi.file_path[i + 1 :]
        }
        return lokasi.file_path
}

main :: proc() {
        ta: mem.Tracking_Allocator
        mem.tracking_allocator_init(&ta, context.allocator)
        defer mem.tracking_allocator_destroy(&ta)

        ctx_awal := context
        context.allocator = mem.tracking_allocator(&ta)

        proses_pengukuran()

        // setiap entri yang masih tersisa di allocation_map adalah kebocoran
        for _, entri in ta.allocation_map {
                fmt.printf("kebocoran %v byte pada %v:%v\n", entri.size, nama_berkas(entri.location), entri.location.line)
        }
        if len(ta.allocation_map) == 0 {
                fmt.println("tidak ada alokasi yang tersisa")
        }

        context = ctx_awal
}
bash
# sumberkan lingkungan sandbox jalur odin bila belum tersedia
[ -f /home/kal/my-project/tools/env.sh ] && source /home/kal/my-project/tools/env.sh
# jalankan program dari dalam folder topik
odin run laporan-kebocoran.odin -file
text Keluaran
kebocoran 12 byte pada laporan-kebocoran.odin:11

Laporan itu terbaca langsung. Ukuran 12 byte berasal dari tiga nilai f32 pada alokasi suhu, sedangkan lokasinya menunjuk baris 11, tepat pada pemanggilan make yang tanpa pasangan delete. Angka setelah nama berkas adalah nomor baris, sehingga penelusuran dimulai dari baris itu, bukan dari pencarian teks ke seluruh program. Program melaporkan nama berkas tanpa jalur foldernya, sehingga keluaran yang sama muncul di mesin mana pun program dijalankan. Alokasi cadangan tidak muncul karena defer melepaskannya sebelum proc berakhir, dan perbedaan antara dua alokasi yang hampir identik itulah yang menunjukkan kekuatan laporan berbasis lokasi sumber.

Versi yang sudah diperbaiki#

Perbaikannya menambah satu baris defer delete(suhu) pada titik alokasi yang bocor. Versi berikut juga menampilkan statistik agregat yang ikut tercatat oleh pelacak.

odin laporan-bersih.odin
package main

import "core:fmt"
import "core:mem"

proses_pengukuran :: proc() {
        // setiap make kini disandingkan dengan delete
        suhu := make([]f32, 3)
        defer delete(suhu)
        suhu[0], suhu[1], suhu[2] = 28.4, 29.1, 30.2

        cadangan := make([]f32, 2)
        defer delete(cadangan)
        cadangan[0], cadangan[1] = 28.4, 29.1
}

main :: proc() {
        ta: mem.Tracking_Allocator
        mem.tracking_allocator_init(&ta, context.allocator)
        defer mem.tracking_allocator_destroy(&ta)

        ctx_awal := context
        context.allocator = mem.tracking_allocator(&ta)

        proses_pengukuran()

        // laporan kebocoran
        for _, entri in ta.allocation_map {
                fmt.printf("kebocoran %v byte pada %v\n", entri.size, entri.location)
        }
        if len(ta.allocation_map) == 0 {
                fmt.println("tidak ada alokasi yang tersisa")
        }

        // statistik yang ikut dicatat tracking allocator
        fmt.printf("total alokasi %v byte dalam %v kali permintaan\n", ta.total_memory_allocated, ta.total_allocation_count)
        fmt.printf("total pelepasan %v byte dalam %v kali pelepasan\n", ta.total_memory_freed, ta.total_free_count)

        context = ctx_awal
}
bash
# sumberkan lingkungan sandbox jalur odin bila belum tersedia
[ -f /home/kal/my-project/tools/env.sh ] && source /home/kal/my-project/tools/env.sh
# jalankan program dari dalam folder topik
odin run laporan-bersih.odin -file
text Keluaran
tidak ada alokasi yang tersisa
total alokasi 20 byte dalam 2 kali permintaan
total pelepasan 20 byte dalam 2 kali pelepasan

Angka 20 byte adalah jumlah dua alokasi, yaitu 12 byte untuk suhu dan 8 byte untuk cadangan, dan total alokasi sama dengan total pelepasan karena keduanya sudah berpasangan. Selisih antara total alokasi dan total pelepasan di akhir program adalah indikator cepat yang tidak memerlukan iterasi map. Field current_memory_allocated menyediakan angka yang sama secara langsung, sedangkan peak_memory_allocated mencatat pemakaian tertinggi sepanjang pemantauan. Satu perbedaan penting pada versi ini terletak pada penempatan baris: defer delete(suhu) ditempelkan tepat di bawah make, sehingga pelepasan tetap berjalan walaupun proc keluar melalui jalur mana pun.

Memasang laporan pada program nyata#

Pola ini dipasang pada proc main ketika program baru dikembangkan, dan dilepas setelah laporan bersih stabil karena pencatatan per alokasi menambah biaya pada map yang tumbuh terus. Pada program yang berjalan dalam putaran kerja, laporan dibaca per titik pemeriksaan yang dipilih, misalnya di setiap akhir sesi pemrosesan batch, alih-alih menunggu akhir program. Pelacak juga menerima alokator penopang apa pun, sehingga arena atau pool yang dibangun dengan tangan dapat dilaporkan kebocorannya dengan cara yang sama.

Dua batasan perlu diingat saat pemasangan. Pelacak pada contoh hanya membungkus context.allocator, sehingga alokasi dari context.temp_allocator tidak masuk laporan, dan pemilihan penopang menentukan apa yang terpantau. Field bad_free_array beserta callback pelepasan liar menyediakan deteksi kedua, yaitu pointer yang dilepaskan tanpa pernah dialokasikan melalui pelacak. Perilaku bawaan deteksi ini adalah menghentikan program agar kesalahan tidak menyebar.

Kesalahan umum#

Kesalahan pertama adalah menghapus pelacak padahal context masih memakainya. Pemanggilan tracking_allocator_destroy membebaskan map-nya, sehingga alokasi yang datang setelahnya menulis pada map yang sudah dilepaskan. Pola pemasangan pada topik ini menghindari urutan itu dengan menyimpan ctx_awal dan memulihkan context sebelum defer penghapusan berjalan di akhir proc.

Kesalahan kedua adalah memeriksa len(ta.allocation_map) di tengah program dan menyimpulkan bahwa programnya bersih. Alokasi yang bocor biasanya terjadi di proc yang belum dipanggil pada titik pemeriksaan itu, sehingga laporan hanya berlaku untuk lingkup yang sudah selesai dijalankan. Pemeriksaan ditempatkan setelah lingkup pemantauan berakhir atau pada titik pemeriksaan yang sengaja dipilih setelah seluruh proc diuji.

Ringkasan#

  • Tracking_Allocator membungkus alokator penopang dan mencatat setiap alokasi di allocation_map beserta ukuran dan lokasi sumbernya.
  • Iterasi allocation_map pada akhir program menghasilkan laporan kebocoran yang menunjuk baris make yang tanpa pasangan delete.
  • Statistik total_memory_allocated dan total_memory_freed memberi indikator selisih alokasi dan pelepasan tanpa membaca map.
  • Pelepasan pointer yang tidak tercatat memicu panic bawaan, sehingga free yang keliru terdeteksi segera.

Referensi#