Mengelola memori secara efektif pada skala Gmail

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Pengantar

Meski JavaScript menggunakan pembersihan sampah memori untuk manajemen memori otomatis, JavaScript bukanlah pengganti manajemen memori yang efektif dalam aplikasi. Aplikasi JavaScript mengalami masalah yang sama terkait memori dengan aplikasi native, seperti kebocoran memori dan penggelembungan, namun aplikasi tersebut juga harus menangani jeda pembersihan sampah memori. Aplikasi berskala besar seperti Gmail menghadapi masalah yang sama dengan aplikasi yang lebih kecil. Baca terus untuk mempelajari bagaimana 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. Tonton video di bawah ini:

Gmail, kami mengalami masalah...

Tim Gmail menghadapi masalah serius. Anekdot dari tab Gmail yang menggunakan beberapa gigabita memori pada laptop dan desktop yang memiliki resource terbatas semakin sering didengar, sering kali dengan kesimpulan mengenai membuat seluruh browser mati. Kisah tentang CPU yang disematkan 100%, aplikasi yang tidak responsif, dan tab sedih Chrome ("Dia mati, Jim."). Tim ini bingung bagaimana cara untuk mulai mendiagnosis masalah, apalagi memperbaikinya. Mereka tidak tahu seberapa luas masalah itu dan alat yang tersedia tidak dapat diperluas ke aplikasi yang besar. Tim ini bekerja sama dengan tim Chrome, dan bersama-sama mereka mengembangkan teknik baru untuk menanggulangi masalah memori, meningkatkan kualitas alat yang ada, dan memungkinkan pengumpulan data memori dari lapangan. Namun, sebelum berlanjut ke alat tersebut, mari kita bahas dasar-dasar manajemen memori JavaScript.

Dasar-Dasar Manajemen Memori

Sebelum dapat mengelola memori di JavaScript secara efektif, Anda harus memahami dasar-dasarnya. Bagian ini akan membahas jenis primitif, grafik objek, dan memberikan definisi untuk penggelembungan memori secara umum dan kebocoran memori dalam JavaScript. Memori dalam JavaScript dapat dikonsepkan sebagai grafik dan karena Graph theory ini berperan dalam manajemen memori JavaScript dan Heap Profiler.

Jenis Primitif

JavaScript memiliki tiga jenis primitif:

  1. Angka (mis. 4, 3.14159)
  2. Boolean (benar atau salah)
  3. String ("Hello World")

Jenis primitif ini tidak dapat mereferensikan nilai lain. Dalam grafik objek, nilai-nilai ini selalu berupa node daun atau akhir, yang berarti nilai-nilai ini tidak pernah memiliki edge keluar.

Hanya ada satu jenis penampung: Objek. Dalam JavaScript, Objek adalah array asosiatif. Objek yang tidak kosong adalah node dalam dengan tepi keluar ke nilai lain (node).

Apa Pengaruhnya pada Array?

Array dalam JavaScript sebenarnya adalah Objek yang memiliki kunci numerik. Ini adalah penyederhanaan, karena waktu proses JavaScript akan mengoptimalkan Objek yang mirip Array dan menampilkannya di balik layar sebagai array.

Terminologi

  1. Nilai - Instance dari jenis primitif, Objek, Array, dll.
  2. Variabel - Nama yang mereferensikan nilai.
  3. 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 mengelola masa pakai root GC, karena root dibuat oleh browser dan dihancurkan saat halaman dihapus muatannya. Variabel global sebenarnya adalah properti di jendela.

Grafik objek

Kapan Suatu Nilai Menjadi Sampah?

Nilai menjadi sampah jika tidak ada jalur dari root ke nilai. Dengan kata lain, memulai dari root dan menelusuri secara menyeluruh semua properti dan variabel Objek yang aktif dalam frame stack, sebuah nilai tidak dapat dicapai, tetapi nilai tersebut telah menjadi sampah.

Grafik sampah

Apa itu Kebocoran Memori di JavaScript?

Kebocoran memori dalam JavaScript paling sering terjadi saat ada simpul DOM yang tidak bisa dijangkau dari hierarki DOM halaman, namun masih direferensikan oleh objek JavaScript. Meskipun browser modern semakin sulit untuk secara tidak sengaja membuat kebocoran, sebenarnya ini masih lebih mudah dari yang dikira. Katakanlah Anda menambahkan sebuah elemen ke hierarki DOM seperti ini:

email.message = document.createElement("div");
displayList.appendChild(email.message);

Kemudian, Anda akan menghapus elemen tersebut dari daftar tampilan:

displayList.removeAllChildren();

Selama email ada, elemen DOM yang dirujuk oleh pesan tidak akan dihapus, meskipun sekarang telah terlepas dari hierarki DOM halaman.

Apa itu Penggembungan?

Halaman membengkak saat Anda menggunakan lebih banyak memori daripada yang diperlukan untuk kecepatan halaman yang optimal. Secara tidak langsung, kebocoran memori juga menyebabkan penggelembungan, tetapi hal ini tidak sesuai dengan desain. Cache aplikasi yang tidak terikat ukuran apa pun adalah sumber umum penggelembungan memori. Selain itu, halaman Anda dapat membengkak oleh data host, misalnya, data piksel yang dimuat dari gambar.

Apa itu Pengumpulan Sampah?

Pembersihan sampah memori adalah cara memori diklaim kembali dalam JavaScript. Browser akan memutuskan kapan hal ini terjadi. Selama pengumpulan, semua eksekusi skrip di halaman Anda akan ditangguhkan saat nilai aktif ditemukan oleh traversal grafik objek yang dimulai di root GC. Semua nilai yang tidak dapat dijangkau diklasifikasikan sebagai sampah. Memori untuk nilai sampah diklaim kembali oleh pengelola memori.

Detail Pengumpul Sampah V8

Untuk membantu memahami lebih lanjut bagaimana pembersihan sampah memori dilakukan, mari kita lihat pembersih sampah memori V8 secara mendetail. V8 menggunakan kolektor generasional. Memori dibagi menjadi dua generasi: muda dan tua. Alokasi dan pengumpulan untuk generasi muda dilakukan dengan cepat dan sering. Alokasi dan pengumpulan pada generasi lama lebih lambat dan lebih jarang.

Kolektor Generasi

V8 menggunakan kolektor dua generasi. Usia nilai didefinisikan sebagai jumlah byte yang dialokasikan sejak telah dialokasikan. Dalam praktiknya, usia suatu nilai sering kali diperkirakan dengan jumlah koleksi generasi muda yang bertahan dari suatu nilai. Setelah suatu nilai cukup tua, nilai tersebut akan dipertahankan ke generasi lama.

Dalam praktiknya, nilai yang baru dialokasikan tidak berumur panjang. Sebuah studi terhadap program Smalltalk menunjukkan bahwa hanya 7% nilai yang dapat bertahan setelah pengumpulan data generasi muda. Studi serupa di seluruh runtime menemukan bahwa rata-rata antara 90% dan 70% nilai yang baru dialokasikan tidak pernah diterapkan ke generasi lama.

Generasi Muda

Heap generasi muda di V8 dibagi menjadi dua ruang, yang diberi nama dari dan ke. Memori dialokasikan dari ke ruang. Pengalokasian aplikasi sangat cepat. Hingga ruang penyimpanan penuh, lalu koleksi generasi muda dipicu. Koleksi generasi muda terlebih dahulu menukar dari dan ke ruang angkasa, yang lama ke luar angkasa (sekarang dari ruang angkasa) akan dipindai dan semua nilai langsung disalin ke ruang atau diterapkan ke generasi lama. Koleksi generasi muda umumnya membutuhkan waktu 10 milidetik (md).

Secara intuitif, Anda harus memahami bahwa setiap alokasi yang dibuat aplikasi Anda akan semakin melelahkan ruang dan menyebabkan jeda GC. Developer game, perhatikan: untuk memastikan waktu render frame 16 md (diperlukan untuk mencapai 60 frame per detik), aplikasi Anda harus nol alokasi, karena satu koleksi generasi muda akan menghabiskan sebagian besar waktu render frame.

Heap generasi muda

Generasi Lama

Heap generasi lama di V8 menggunakan algoritma mark-compact untuk pengumpulan gambar. Alokasi generasi lama terjadi setiap kali nilai dipertahankan dari generasi muda hingga generasi tua. Setiap kali koleksi generasi lama muncul, koleksi generasi muda juga ikut dilakukan. Aplikasi Anda akan dijeda pada urutan detik. Dalam praktiknya, hal ini dapat diterima karena koleksi generasi lama jarang.

Ringkasan GC V8

Manajemen memori otomatis dengan pembersihan sampah memori sangat cocok untuk produktivitas developer, tetapi setiap kali Anda mengalokasikan nilai, Anda akan semakin dekat dengan jeda pembersihan sampah memori. Jeda pembersihan sampah memori dapat merusak nuansa aplikasi Anda karena menyebabkan jank. Setelah memahami cara JavaScript mengelola memori, Anda dapat membuat pilihan yang tepat untuk aplikasi.

Memperbaiki Gmail

Selama setahun terakhir, banyak fitur dan perbaikan bug yang masuk ke Chrome DevTools dan membuatnya lebih andal dari sebelumnya. Selain itu, browser itu sendiri membuat perubahan penting pada performance.memory API yang memungkinkan Gmail dan aplikasi lain mengumpulkan statistik memori dari kolom. Berbekal alat-alat luar biasa ini, tugas yang dahulu tampak seperti tugas yang mustahil, kini menjadi game yang seru untuk melacak penyebabnya.

Alat dan Teknik

Data Kolom dan performance.memory API

Mulai Chrome 22, performance.memory API diaktifkan secara default. Untuk aplikasi jangka panjang seperti Gmail, data dari pengguna sungguhan sangat berharga. Informasi ini memungkinkan kami membedakan antara pengguna super-- yang menghabiskan 8-16 jam sehari di Gmail, menerima ratusan pesan sehari-- dari lebih banyak pengguna rata-rata yang menghabiskan beberapa menit per hari di Gmail, menerima selusin pesan atau lebih dalam seminggu.

API ini menampilkan tiga data:

  1. jsHeapSizeLimit - Jumlah memori (dalam byte) yang dibatasi untuk heap JavaScript.
  2. totalJSHeapSize - Jumlah memori (dalam byte) yang dialokasikan oleh heap JavaScript termasuk ruang kosong.
  3. useJSHeapSize - Jumlah memori (dalam byte) yang saat ini digunakan.

Satu hal yang perlu diingat adalah API tersebut 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 dari tab browser lain selain yang berisi aplikasi Anda.

Mengukur Memori dalam Skala Besar

Gmail menginstrumentasikan JavaScript-nya untuk menggunakan performance.memory API guna mengumpulkan informasi memori kira-kira sekali setiap 30 menit. Karena banyak pengguna Gmail tidak menggunakan aplikasi selama berhari-hari, tim dapat melacak pertumbuhan memori dari waktu ke waktu serta statistik jejak memori secara keseluruhan. Dalam beberapa hari setelah melengkapi Gmail untuk mengumpulkan informasi memori dari pengambilan sampel acak pengguna, tim memiliki cukup data untuk memahami seberapa luas masalah memori di antara kebanyakan pengguna. Mereka menetapkan dasar pengukuran dan menggunakan aliran data yang masuk untuk melacak kemajuan dalam tujuan mengurangi konsumsi memori. Pada akhirnya, data ini juga akan digunakan untuk menangkap regresi memori.

Di samping tujuan pelacakan, pengukuran lapangan juga memberikan insight mendalam tentang korelasi antara jejak memori dan performa aplikasi. Berlawanan dengan kepercayaan populer bahwa "lebih banyak memori menghasilkan performa yang lebih baik", tim Gmail menemukan bahwa semakin besar jejak memori, latensi yang lebih lama untuk tindakan Gmail umum. Berbekal pengungkapan ini, mereka semakin termotivasi untuk mengendalikan konsumsi memori mereka.

Mengukur Memori dalam Skala Besar

Mengidentifikasi Masalah Memori dengan Linimasa DevTools

Langkah pertama dalam memecahkan masalah performa adalah membuktikan bahwa masalah tersebut memang ada, membuat pengujian yang dapat direproduksi, dan melakukan pengukuran dasar masalah. Tanpa program yang dapat direproduksi, Anda tidak dapat mengukur masalah dengan andal. Tanpa pengukuran dasar pengukuran, Anda tidak dapat mengetahui seberapa banyak peningkatan performa telah terjadi.

Panel Timeline DevTools adalah kandidat ideal untuk membuktikan bahwa masalah tersebut ada. Panduan ini memberikan ringkasan lengkap tentang tempat waktu yang dihabiskan saat memuat dan berinteraksi dengan aplikasi web atau halaman Anda. Semua peristiwa, mulai dari memuat resource hingga mengurai JavaScript, menghitung gaya, jeda pembersihan sampah memori, dan menggambar ulang diplot pada linimasa. Untuk tujuan penyelidikan masalah memori, panel Rentang Waktu juga memiliki mode Memori yang melacak total memori yang dialokasikan, jumlah simpul DOM, jumlah objek jendela, dan jumlah pemroses kejadian yang dialokasikan.

Membuktikan adanya masalah

Mulailah dengan mengidentifikasi urutan tindakan yang Anda curigai sebagai kebocoran memori. Mulai merekam linimasa, lalu melakukan urutan tindakan. Gunakan tombol tempat sampah di bagian bawah untuk memaksa pembersihan sampah memori penuh. Jika, setelah beberapa iterasi, Anda melihat grafik berbentuk gigi gergaji, berarti Anda mengalokasikan banyak objek yang berumur pendek. Namun, jika urutan tindakan tidak diperkirakan akan menghasilkan memori yang tersisa, dan jumlah node DOM tidak turun kembali ke dasar pengukuran tempat Anda memulai, Anda memiliki alasan yang baik untuk mencurigai adanya kebocoran.

Grafik berbentuk gigi gergaji

Setelah mengonfirmasi bahwa masalah tersebut ada, Anda bisa mendapatkan bantuan untuk mengidentifikasi sumber masalah dari DevTools Heap Profiler.

Menemukan Kebocoran Memori dengan DevTools Heap Profiler

Panel Profiler menyediakan CPU profiler dan profiler Heap. Pembuatan profil heap berfungsi dengan mengambil snapshot grafik objek. Sebelum snapshot diambil, generasi muda dan tua akan dibersihkan sampah memorinya. 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 terperinci dapat ditemukan di situs Chrome Developers. Kita akan berfokus di sini tentang profiler Alokasi Heap.

Menggunakan Heap Allocation Profiler

Profiler Heap Allocation menggabungkan informasi snapshot mendetail dari Heap Profiler dengan update dan pelacakan bertahap pada panel Timeline. Buka panel Profiles, mulai profil Record Heap Allocations, lakukan serangkaian tindakan, lalu hentikan perekaman untuk dianalisis. Profiler alokasi mengambil snapshot heap secara berkala selama perekaman (sesering setiap 50 md!) dan satu snapshot akhir di akhir perekaman.

Profiler alokasi heap

Batang di bagian atas menunjukkan kapan objek baru ditemukan di heap. Ketinggian setiap bilah sesuai dengan ukuran objek yang baru dialokasikan, dan warna bilah menunjukkan apakah objek tersebut masih aktif atau tidak di cuplikan heap akhir: bilah biru menunjukkan objek yang masih aktif di akhir linimasa, bilah abu-abu menunjukkan objek yang dialokasikan selama linimasa, namun sejak saat itu telah dibersihkan sampah memori.

Pada contoh di atas, suatu tindakan dilakukan 10 kali. Program contoh meng-cache lima objek, sehingga lima batang biru terakhir adalah yang diharapkan. Tetapi bilah biru paling kiri menunjukkan potensi masalah. Kemudian Anda dapat menggunakan penggeser pada linimasa di atas untuk memperbesar snapshot tertentu tersebut dan melihat objek yang baru saja dialokasikan di titik tersebut. Mengklik objek tertentu dalam heap akan menampilkan pohon yang menahannya di bagian bawah cuplikan heap. Dengan memeriksa jalur penahan ke objek, Anda akan mendapatkan informasi yang cukup untuk memahami alasan objek tidak dikumpulkan, dan Anda dapat membuat perubahan kode yang diperlukan untuk menghapus referensi yang tidak perlu.

Menyelesaikan Krisis Memori Gmail

Dengan menggunakan alat dan teknik yang dibahas di atas, tim Gmail dapat mengidentifikasi beberapa kategori bug: cache tak terbatas, array callback yang terus bertambah dan menunggu sesuatu terjadi yang tidak pernah benar-benar terjadi, dan pemroses peristiwa secara tidak sengaja mempertahankan target mereka. Dengan memperbaiki masalah ini, penggunaan memori Gmail secara keseluruhan berkurang secara dramatis. Pengguna pada 99% persen menggunakan memori 80% lebih sedikit daripada sebelumnya dan konsumsi memori pengguna median turun hampir 50%.

Penggunaan memori Gmail

Karena Gmail menggunakan lebih sedikit memori, latensi jeda GC berkurang, sehingga meningkatkan pengalaman pengguna secara keseluruhan.

Selain itu, ketika tim Gmail mengumpulkan statistik tentang penggunaan memori, mereka dapat mengungkap regresi pembersihan sampah memori di dalam Chrome. Secara khusus, dua bug fragmentasi ditemukan saat data memori Gmail mulai menunjukkan peningkatan drastis dalam kesenjangan antara total memori yang dialokasikan dan memori live.

Pesan Ajakan

Tanyakan pada diri Anda pertanyaan-pertanyaan berikut:

  1. Berapa banyak memori yang digunakan aplikasi saya? Mungkin Anda menggunakan terlalu banyak memori yang bertentangan dengan keyakinan umum yang berdampak negatif pada performa aplikasi secara keseluruhan. Sulit untuk mengetahui secara pasti berapa angka yang tepat, namun pastikan untuk memverifikasi bahwa setiap cache tambahan yang digunakan halaman Anda memiliki dampak performa yang terukur.
  2. Apakah halaman saya bebas bocor? Jika halaman Anda mengalami kebocoran memori, hal ini tidak hanya dapat memengaruhi performa halaman, tetapi juga terhadap tab lainnya. Gunakan pelacak objek untuk membantu mempersempit pencarian tentang kebocoran.
  3. Seberapa sering GCing halaman saya dilakukan? Anda dapat melihat penjedaan GC menggunakan panel Linimasa di Chrome Developer Tools. Jika halaman Anda sering melakukan GCing, kemungkinan Anda terlalu sering mengalokasikan konten, sehingga meninggalkan memori generasi muda Anda.

Kesimpulan

Kami memulainya dalam krisis. Mencakup dasar-dasar inti manajemen memori dalam JavaScript dan V8 secara khusus. Anda telah mempelajari cara menggunakan alat tersebut, termasuk fitur pelacak objek baru yang tersedia di build Chrome terbaru. Tim Gmail, dengan berbekal pengetahuan ini, berhasil memecahkan masalah penggunaan memori dan meraih peningkatan performa. Anda dapat melakukan hal yang sama dengan aplikasi web Anda!