Pengantar
Meskipun JavaScript menggunakan pembersihan sampah memori untuk pengelolaan memori otomatis, JavaScript bukanlah pengganti pengelolaan memori yang efektif dalam aplikasi. Aplikasi JavaScript mengalami masalah terkait memori yang sama seperti aplikasi native, seperti kebocoran dan penggelembungan memori, tetapi aplikasi tersebut juga harus menangani jeda pembersihan sampah memori. Aplikasi skala besar seperti Gmail mengalami masalah yang sama dengan aplikasi Anda yang lebih kecil. Baca terus untuk mempelajari cara tim Gmail menggunakan Chrome DevTools untuk mengidentifikasi, mengisolasi, dan memperbaiki masalah memori mereka.
Sesi Google I/O 2013
Kami mempresentasikan materi ini di Google I/O 2013. Lihat video di bawah:
Gmail, ada masalah…
Tim Gmail menghadapi masalah serius. Anekdot tentang tab Gmail yang menghabiskan beberapa gigabyte memori di laptop dan desktop yang memiliki keterbatasan resource semakin sering terdengar, sering kali dengan kesimpulan bahwa tab tersebut membuat seluruh browser tidak berfungsi. Cerita tentang CPU yang terpaku pada 100%, aplikasi yang tidak responsif, dan tab Chrome yang tidak berfungsi ("Dia mati, Jim"). Tim tidak tahu cara memulai mendiagnosis masalah, apalagi memperbaikinya. Mereka tidak tahu seberapa luas masalahnya dan alat yang tersedia tidak dapat diskalakan ke aplikasi besar. Tim ini bergabung dengan tim Chrome, dan bersama-sama mereka mengembangkan teknik baru untuk melakukan triage masalah memori, meningkatkan kualitas alat yang ada, dan memungkinkan pengumpulan data memori dari lapangan. Namun, sebelum membahas alat tersebut, mari kita bahas dasar-dasar pengelolaan memori JavaScript.
Dasar-Dasar Pengelolaan Memori
Sebelum dapat mengelola memori secara efektif di JavaScript, Anda harus memahami dasar-dasarnya. Bagian ini akan membahas jenis primitif, grafik objek, dan memberikan definisi untuk pembengkakan memori secara umum dan kebocoran memori di JavaScript. Memori di JavaScript dapat dikonseptualisasikan sebagai grafik dan karenanya Teori grafik berperan dalam pengelolaan memori JavaScript dan Heap Profiler.
Jenis Primitif
JavaScript memiliki tiga jenis primitif:
- Angka (mis. 4, 3,14159)
- Boolean (benar atau salah)
- String ("Hello World")
Jenis primitif ini tidak dapat mereferensikan nilai lain. Dalam grafik objek, nilai ini selalu berupa node daun atau node akhir, yang berarti nilai tersebut tidak pernah memiliki tepi keluar.
Hanya ada satu jenis penampung: Objek. Di JavaScript, Objek adalah array asosiatif. Objek yang tidak kosong adalah node dalam dengan tepi keluar ke nilai (node) lain.
Bagaimana dengan Array?
Array di JavaScript sebenarnya adalah Objek yang memiliki kunci numerik. Ini adalah penyederhanaan, karena runtime JavaScript akan mengoptimalkan Objek seperti Array dan menampilkannya di balik layar sebagai array.
Terminologi
- Nilai - Instance dari jenis primitif, Objek, Array, dll.
- Variabel - Nama yang mereferensikan nilai.
- Properti - Nama dalam Objek yang mereferensikan nilai.
Grafik Objek
Semua nilai dalam JavaScript adalah bagian dari grafik objek. Grafik dimulai dengan root, misalnya, objek jendela. Anda tidak dapat mengontrol masa aktif root GC karena root GC dibuat oleh browser dan dihancurkan saat halaman di-unload. Variabel global sebenarnya adalah properti di jendela.
Kapan Nilai Menjadi Sampah?
Nilai menjadi sampah jika tidak ada jalur dari root ke nilai. Dengan kata lain, dimulai dari root dan menelusuri semua properti dan variabel Objek yang aktif di frame stack secara menyeluruh, nilai tidak dapat dijangkau, nilai tersebut telah menjadi sampah.
Apa yang dimaksud dengan Kebocoran Memori di JavaScript?
Kebocoran memori di JavaScript paling sering terjadi saat ada node DOM yang tidak dapat dijangkau dari hierarki DOM halaman, tetapi masih dirujuk oleh objek JavaScript. Meskipun browser modern semakin mempersulit kebocoran yang tidak disengaja, hal ini masih lebih mudah daripada yang dibayangkan. Misalnya, Anda menambahkan elemen ke hierarki DOM seperti ini:
email.message = document.createElement("div");
displayList.appendChild(email.message);
Kemudian, Anda menghapus elemen dari daftar tampilan:
displayList.removeAllChildren();
Selama email
ada, elemen DOM yang dirujuk oleh pesan tidak akan dihapus, meskipun sekarang elemen tersebut dilepaskan dari hierarki DOM halaman.
Apa yang dimaksud dengan Bloat?
Halaman Anda membengkak jika Anda menggunakan lebih banyak memori daripada yang diperlukan untuk kecepatan halaman yang optimal. Secara tidak langsung, kebocoran memori juga menyebabkan pemborosan, tetapi hal ini bukan karena desain. Cache aplikasi yang tidak memiliki batas ukuran adalah sumber umum pemborosan memori. Selain itu, halaman Anda dapat membengkak karena data host, misalnya, data piksel yang dimuat dari gambar.
Apa yang dimaksud dengan Pembersihan Sampah Memori?
Pengumpulan sampah adalah cara memori diklaim kembali di JavaScript. Browser memutuskan kapan tindakan ini terjadi. Selama pengumpulan, semua eksekusi skrip di halaman Anda ditangguhkan saat nilai aktif ditemukan oleh penelusuran grafik objek yang dimulai dari root GC. Semua nilai yang tidak dapat dijangkau diklasifikasikan sebagai sampah. Memori untuk nilai sampah diklaim kembali oleh pengelola memori.
Pengumpul Sampah V8 secara Mendetail
Untuk membantu lebih memahami cara kerja pembersihan sampah memori, mari kita lihat pembersihan sampah memori V8 secara mendetail. V8 menggunakan kolektor generasi. Memori dibagi menjadi dua generasi: yang baru dan yang lama. Alokasi dan pengumpulan dalam generasi muda cepat dan sering. Alokasi dan pengumpulan dalam generasi lama lebih lambat dan lebih jarang.
Kolektor Generasional
V8 menggunakan kolektor dua generasi. Usia nilai ditentukan sebagai jumlah byte yang dialokasikan sejak dialokasikan. Dalam praktiknya, usia nilai sering kali diperkirakan dengan jumlah koleksi generasi muda yang bertahan. Setelah nilai cukup lama, nilai tersebut akan ditransfer ke generasi lama.
Dalam praktiknya, nilai yang baru dialokasikan tidak bertahan lama. Studi tentang program Smalltalk menunjukkan bahwa hanya 7% nilai yang bertahan setelah pengumpulan generasi muda. Studi serupa di seluruh runtime menemukan bahwa rata-rata antara 90% dan 70% nilai yang baru dialokasikan tidak pernah memiliki masa berlaku ke generasi lama.
Generasi Muda
Heap generasi muda di V8 dibagi menjadi dua ruang, yang diberi nama dari dan ke. Memori dialokasikan dari ruang ke. Alokasi sangat cepat, hingga ruang ke penuh, pada saat itu pengumpulan generasi muda dipicu. Pengumpulan generasi muda pertama-tama menukar ruang dari dan ke, ruang ke lama (sekarang ruang dari) dipindai dan semua nilai aktif disalin ke ruang ke atau dipegang ke generasi lama. Pengumpulan generasi muda yang khas akan memerlukan waktu sekitar 10 milidetik (md).
Secara intuitif, Anda harus memahami bahwa setiap alokasi yang dilakukan aplikasi akan membuat Anda lebih dekat ke kehabisan ruang dan menyebabkan jeda GC. Developer game, perhatikan: untuk memastikan waktu frame 16 md (diperlukan untuk mencapai 60 frame per detik), aplikasi Anda harus membuat alokasi nol, karena satu koleksi generasi muda akan menghabiskan sebagian besar waktu frame.
Generasi Lama
Heap generasi lama di V8 menggunakan algoritma mark-compact untuk pengumpulan. Alokasi generasi lama terjadi setiap kali nilai memiliki masa berlaku dari generasi muda ke generasi lama. Setiap kali pengumpulan generasi lama terjadi, pengumpulan generasi baru juga dilakukan. Aplikasi Anda akan dijeda dalam hitungan detik. Dalam praktiknya, hal ini dapat diterima karena koleksi generasi lama jarang terjadi.
Ringkasan GC V8
Pengelolaan memori otomatis dengan pembersihan sampah memori sangat bagus untuk produktivitas developer, tetapi setiap kali Anda mengalokasikan nilai, Anda semakin mendekati jeda pembersihan sampah memori. Jeda pembersihan sampah memori dapat merusak nuansa aplikasi Anda dengan menyebabkan jank. Setelah memahami cara JavaScript mengelola memori, Anda dapat membuat pilihan yang tepat untuk aplikasi Anda.
Memperbaiki Gmail
Selama setahun terakhir, banyak fitur dan perbaikan bug telah ditambahkan ke Chrome DevTools sehingga menjadi lebih canggih dari sebelumnya. Selain itu, browser itu sendiri membuat perubahan utama pada performance.memory API sehingga Gmail dan aplikasi lainnya dapat mengumpulkan statistik memori dari kolom. Dengan alat yang luar biasa ini, tugas yang sebelumnya tampak mustahil akan segera menjadi permainan seru untuk melacak pelaku.
Alat dan Teknik
Data Kolom dan performance.memory API
Mulai Chrome 22, performance.memory API diaktifkan secara default. Untuk aplikasi yang berjalan lama seperti Gmail, data dari pengguna yang sebenarnya sangat berharga. Informasi ini memungkinkan kami membedakan antara pengguna berat -- pengguna yang menghabiskan 8-16 jam sehari di Gmail, menerima ratusan pesan sehari -- dari pengguna biasa yang menghabiskan beberapa menit sehari di Gmail, menerima sekitar selusin pesan seminggu.
API ini menampilkan tiga bagian data:
- jsHeapSizeLimit - Jumlah memori (dalam byte) yang dibatasi untuk heap JavaScript.
- totalJSHeapSize - Jumlah memori (dalam byte) yang telah dialokasikan heap JavaScript termasuk ruang kosong.
- usedJSHeapSize - Jumlah memori (dalam byte) yang sedang digunakan.
Satu hal yang perlu diingat adalah API menampilkan nilai memori untuk seluruh proses Chrome. Meskipun bukan mode default, dalam keadaan tertentu, Chrome dapat membuka beberapa tab dalam proses perender yang sama. Artinya, nilai yang ditampilkan oleh performance.memory dapat berisi jejak memori tab browser lain selain tab yang berisi aplikasi Anda.
Mengukur Memori dalam Skala Besar
Gmail melengkapi JavaScript-nya untuk menggunakan performance.memory API guna mengumpulkan informasi memori sekitar sekali setiap 30 menit. Karena banyak pengguna Gmail membiarkan aplikasi berjalan selama berhari-hari, tim dapat melacak pertumbuhan memori dari waktu ke waktu serta statistik jejak memori secara keseluruhan. Dalam beberapa hari setelah melakukan instrumentasi Gmail untuk mengumpulkan informasi memori dari sampel pengguna secara acak, tim memiliki cukup data untuk memahami seberapa luas masalah memori di kalangan pengguna rata-rata. Mereka menetapkan dasar pengukuran dan menggunakan aliran data yang masuk untuk melacak progres menuju sasaran pengurangan konsumsi memori. Pada akhirnya, data ini juga akan digunakan untuk menangkap regresi memori.
Selain tujuan pelacakan, pengukuran lapangan juga memberikan insight yang tajam tentang korelasi antara jejak memori dan performa aplikasi. Berbeda dengan anggapan umum bahwa "lebih banyak memori akan menghasilkan performa yang lebih baik", tim Gmail menemukan bahwa semakin besar jejak memori, semakin lama latensi untuk tindakan Gmail umum. Dengan mengetahui hal ini, mereka semakin termotivasi untuk mengurangi konsumsi memori.
Mengidentifikasi Masalah Memori dengan Linimasa DevTools
Langkah pertama dalam memecahkan masalah performa adalah membuktikan bahwa masalah tersebut ada, membuat pengujian yang dapat direproduksi, dan melakukan pengukuran dasar pengukuran masalah. Tanpa program yang dapat direproduksi, Anda tidak dapat mengukur masalah dengan andal. Tanpa pengukuran dasar pengukuran, Anda tidak akan tahu seberapa besar peningkatan performa yang telah Anda lakukan.
Panel Linimasa DevTools adalah kandidat yang ideal untuk membuktikan bahwa masalah tersebut ada. Laporan ini memberikan ringkasan lengkap tentang waktu yang dihabiskan saat memuat dan berinteraksi dengan aplikasi atau halaman web Anda. Semua peristiwa, mulai dari memuat resource hingga mengurai JavaScript, menghitung gaya, menjeda pengumpulan sampah, dan mewarnai ulang diplot pada linimasa. Untuk tujuan menyelidiki masalah memori, panel Linimasa juga memiliki mode Memori yang melacak total memori yang dialokasikan, jumlah node DOM, jumlah objek jendela, dan jumlah pemroses peristiwa yang dialokasikan.
Membuktikan bahwa masalah ada
Mulailah dengan mengidentifikasi urutan tindakan yang Anda curigai mengalami kebocoran memori. Mulai rekam linimasa, lalu lakukan urutan tindakan. Gunakan tombol tempat sampah di bagian bawah untuk memaksa pembersihan sampah memori penuh. Jika, setelah beberapa iterasi, Anda melihat grafik berbentuk gergaji, berarti Anda mengalokasikan banyak objek yang berumur pendek. Namun, jika urutan tindakan tidak diharapkan menghasilkan memori yang dipertahankan, dan jumlah node DOM tidak turun kembali ke dasar pengukuran tempat Anda memulai, Anda memiliki alasan yang kuat untuk mencurigai adanya kebocoran.
Setelah mengonfirmasi bahwa masalah tersebut ada, Anda bisa mendapatkan bantuan untuk mengidentifikasi sumber masalah dari Profiler Heap DevTools.
Menemukan Kebocoran Memori dengan Profiler Heap DevTools
Panel Profiler menyediakan profiler CPU dan profiler Heap. Pembuatan profil heap berfungsi dengan mengambil snapshot grafik objek. Sebelum snapshot diambil, generasi lama dan baru akan dibersihkan. Dengan kata lain, Anda hanya akan melihat nilai yang aktif saat snapshot diambil.
Ada terlalu banyak fungsi di Profiler heap untuk dibahas secara memadai dalam artikel ini, tetapi dokumentasi mendetail dapat ditemukan di situs Developer Chrome. Di sini, kita akan berfokus pada profiler Alokasi Heap.
Menggunakan Heap Allocation Profiler
Profiler Alokasi Heap menggabungkan informasi snapshot mendetail dari Heap Profiler dengan pembaruan dan pelacakan inkremental panel Linimasa. Buka panel Profil, mulai profil Record Heap Allocations, lakukan urutan tindakan, lalu hentikan perekaman untuk analisis. Profiler alokasi mengambil snapshot heap secara berkala selama perekaman (hingga setiap 50 md!) dan satu snapshot final di akhir perekaman.
Batang di bagian atas menunjukkan waktu ditemukannya objek baru di heap. Tinggi setiap batang sesuai dengan ukuran objek yang baru dialokasikan, dan warna batang menunjukkan apakah objek tersebut masih aktif atau tidak dalam snapshot heap akhir: batang biru menunjukkan objek yang masih aktif di akhir linimasa, batang abu-abu menunjukkan objek yang dialokasikan selama linimasa, tetapi telah dihapus sampahnya.
Pada contoh di atas, tindakan dilakukan 10 kali. Program contoh menyimpan lima objek di cache, sehingga lima bilah biru terakhir memang sudah diperkirakan. Namun, bilah biru paling kiri menunjukkan potensi masalah. Anda kemudian dapat menggunakan penggeser di linimasa di atas untuk memperbesar snapshot tersebut dan melihat objek yang baru-baru ini dialokasikan pada titik tersebut. Mengeklik objek tertentu di heap akan menampilkan hierarki retensinya di bagian bawah snapshot heap. Memeriksa jalur yang menahan ke objek akan memberi Anda informasi yang cukup untuk memahami mengapa objek tidak dikumpulkan dan Anda bisa membuat perubahan kode yang diperlukan untuk membuang referensi yang tidak perlu.
Menyelesaikan Krisis Memori Gmail
Dengan menggunakan alat dan teknik yang telah dibahas di atas, tim Gmail dapat mengidentifikasi beberapa kategori bug: cache tanpa batas, array callback yang terus bertambah dan menunggu sesuatu terjadi yang sebenarnya tidak pernah terjadi, dan pemroses peristiwa yang tidak sengaja mempertahankan targetnya. Dengan memperbaiki masalah ini, penggunaan memori Gmail secara keseluruhan berkurang secara drastis. Pengguna dalam persentase 99% menggunakan memori 80% lebih sedikit dari sebelumnya dan konsumsi memori pengguna median menurun hampir 50%.
Karena Gmail menggunakan lebih sedikit memori, latensi jeda GC berkurang, sehingga meningkatkan pengalaman pengguna secara keseluruhan.
Selain itu, dengan tim Gmail mengumpulkan statistik tentang penggunaan memori, mereka dapat menemukan regresi pembersihan sampah di dalam Chrome. Secara khusus, dua bug fragmentasi ditemukan saat data memori Gmail mulai menunjukkan peningkatan yang signifikan dalam kesenjangan antara total memori yang dialokasikan dan memori aktif.
Pesan Ajakan
Tanyakan pada diri Anda sendiri pertanyaan-pertanyaan berikut:
- Berapa banyak memori yang digunakan aplikasi saya? Anda mungkin menggunakan terlalu banyak memori yang bertentangan dengan kepercayaan umum yang memiliki dampak negatif bersih pada performa aplikasi secara keseluruhan. Sulit untuk mengetahui angka yang tepat, tetapi pastikan untuk memverifikasi bahwa cache tambahan yang digunakan halaman Anda memiliki dampak performa yang dapat diukur.
- Apakah halaman saya bebas kebocoran? Jika halaman Anda mengalami kebocoran memori, hal ini tidak hanya dapat memengaruhi performa halaman, tetapi juga tab lainnya. Gunakan pelacak objek untuk membantu mempersempit kebocoran.
- Seberapa sering halaman saya melakukan GC? Anda dapat melihat jeda GC menggunakan panel Linimasa di Chrome Developer Tools. Jika halaman Anda sering melakukan GC, kemungkinan Anda terlalu sering mengalokasikan, sehingga menghabiskan memori generasi muda.
Kesimpulan
Kami memulainya dalam krisis. Membahas dasar-dasar inti pengelolaan memori di JavaScript dan V8 secara khusus. Anda telah mempelajari cara menggunakan alat ini, termasuk fitur pelacak objek baru yang tersedia di build Chrome terbaru. Dengan pengetahuan ini, tim Gmail berhasil menyelesaikan masalah penggunaan memori dan meningkatkan performa. Anda dapat melakukan hal yang sama dengan aplikasi web Anda.