Menemukan kebocoran memori JavaScript dengan Chrome

163

Saya telah membuat test case yang sangat sederhana yang menciptakan tampilan Backbone, melampirkan handler ke suatu peristiwa, dan membuat instance kelas yang ditentukan pengguna. Saya percaya bahwa dengan mengklik tombol "Hapus" dalam sampel ini, semuanya akan dibersihkan dan seharusnya tidak ada kebocoran memori.

Jsfiddle untuk kode ada di sini: http://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

Namun, saya tidak jelas bagaimana cara menggunakan profiler Google Chrome untuk memverifikasi bahwa ini adalah kenyataannya. Ada trilyun hal yang muncul di snapshot profiler tumpukan, dan saya tidak tahu bagaimana untuk memecahkan kode apa yang baik / buruk. Tutorial yang saya lihat sejauh ini hanya memberi tahu saya untuk "menggunakan snapshot profiler" atau memberi saya manifesto yang sangat rinci tentang cara kerja seluruh profiler. Apakah mungkin hanya menggunakan profiler sebagai alat, atau apakah saya benar-benar harus memahami bagaimana semuanya direkayasa?

EDIT: Tutorial seperti ini:

Memperbaiki kebocoran memori Gmail

Menggunakan DevTools

Merupakan perwakilan dari beberapa materi kuat di luar sana, dari apa yang saya lihat. Namun, di luar memperkenalkan konsep 3 Snapshot Technique , saya menemukan mereka menawarkan sangat sedikit dalam hal pengetahuan praktis (untuk pemula seperti saya). Tutorial 'Menggunakan DevTools' tidak bekerja melalui contoh nyata, jadi deskripsi konseptualnya yang kabur dan umum tidak terlalu membantu. Adapun contoh 'Gmail':

Jadi Anda menemukan kebocoran. Sekarang apa?

  • Periksa jalur penahan benda bocor di bagian bawah panel Profil

  • Jika situs alokasi tidak dapat disimpulkan dengan mudah (mis. Pendengar acara):

  • Instrumen konstruktor objek penahan melalui konsol JS untuk menyimpan jejak stack untuk alokasi

  • Menggunakan Penutupan? Aktifkan flag yang sesuai yang ada (yaitu goog.events.Listener.ENABLE_MONITORING) untuk mengatur properti creationStack selama konstruksi

Saya menemukan diri saya lebih bingung setelah membaca itu, tidak kurang. Dan, sekali lagi, ini hanya memberitahu saya untuk melakukan sesuatu, bukan bagaimana melakukannya. Dari sudut pandang saya, semua informasi di luar sana terlalu samar atau hanya akan masuk akal bagi seseorang yang sudah memahami prosesnya.

Beberapa masalah yang lebih spesifik ini telah diangkat dalam jawaban @Jonathan Naguin di bawah ini.

EleventyOne
sumber
2
Saya tidak tahu apa-apa tentang pengujian penggunaan memori di browser, tetapi jika Anda belum melihatnya, artikel Addy Osmani tentang pengawas web Chrome mungkin bisa membantu.
Paul D. Waite
1
Terima kasih untuk sarannya, Paul. Namun, ketika saya mengambil satu snapshot sebelum mengklik hapus, dan kemudian yang lain setelah diklik, lalu pilih 'objek yang dialokasikan antara snapshot 1 dan 2' (seperti yang disarankan dalam artikelnya) masih ada lebih dari 2000 objek hadir. Ada 4 entri 'HTMLButtonElement', misalnya, yang tidak masuk akal bagi saya. Sungguh, saya tidak tahu apa yang sedang terjadi.
EleventyOne
3
doh, itu tidak terdengar sangat membantu. Mungkin dengan bahasa yang dikumpulkan sampah seperti JavaScript, Anda tidak benar-benar dimaksudkan untuk memverifikasi apa yang Anda lakukan dengan memori pada tingkat granular seperti pengujian Anda. Cara yang lebih baik untuk memeriksa kebocoran memori mungkin dengan menelepon main10.000 kali, bukan sekali, dan melihat apakah Anda memiliki lebih banyak memori yang digunakan di akhir.
Paul D. Waite
3
@ PaulD. Ya, mungkin. Tetapi bagi saya tampaknya saya masih memerlukan analisis tingkat granular untuk menentukan dengan tepat apa masalahnya, daripada hanya bisa mengatakan (atau tidak mengatakan): "Oke, ada masalah memori di suatu tempat di sini". Dan saya mendapatkan kesan bahwa saya harus dapat menggunakan profiler mereka pada tingkat granular ... Saya hanya tidak yakin bagaimana :(
EleventyOne
Anda harus melihat di youtube.com/watch?v=L3ugr9BJqIs
maja

Jawaban:

205

Alur kerja yang baik untuk menemukan kebocoran memori adalah teknik snapshot tiga , pertama kali digunakan oleh Loreena Lee dan tim Gmail untuk menyelesaikan beberapa masalah memori mereka. Langkah-langkahnya adalah, secara umum:

  • Ambil snapshot tumpukan.
  • Lakukan hal-hal.
  • Ambil snapshot tumpukan lain.
  • Ulangi hal yang sama.
  • Ambil snapshot tumpukan lain.
  • Memfilter objek yang dialokasikan antara Snapshots 1 dan 2 dalam tampilan "Ringkasan" Snapshot 3.

Sebagai contoh Anda, saya telah mengadaptasi kode untuk menunjukkan proses ini (Anda dapat menemukannya di sini ) menunda pembuatan Backbone View hingga acara klik tombol Start. Sekarang:

  • Jalankan HTML (disimpan secara lokal menggunakan alamat ini ) dan ambil snapshot.
  • Klik Mulai untuk membuat tampilan.
  • Ambil snapshot lain.
  • Klik hapus.
  • Ambil snapshot lain.
  • Memfilter objek yang dialokasikan antara Snapshots 1 dan 2 dalam tampilan "Ringkasan" Snapshot 3.

Sekarang Anda siap menemukan kebocoran memori!

Anda akan melihat beberapa warna yang berbeda. Node merah tidak memiliki referensi langsung dari Javascript kepada mereka, tetapi masih hidup karena mereka adalah bagian dari pohon DOM yang terpisah. Mungkin ada simpul di pohon yang dirujuk dari Javascript (mungkin sebagai penutupan atau variabel) tetapi secara kebetulan mencegah seluruh pohon DOM dari sampah yang dikumpulkan.

masukkan deskripsi gambar di sini

Namun node kuning memiliki referensi langsung dari Javascript. Cari node kuning di pohon DOM terpisah yang sama untuk mencari referensi dari Javascript Anda. Harus ada rantai properti yang mengarah dari jendela DOM ke elemen.

Secara khusus, Anda dapat melihat elemen HTML Div ditandai dengan warna merah. Jika Anda memperluas elemen Anda akan melihat yang direferensikan oleh fungsi "cache".

masukkan deskripsi gambar di sini

Pilih baris dan di konsol Anda ketik $ 0, Anda akan melihat fungsi dan lokasi aktual:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

Di sinilah elemen Anda dirujuk. Sayangnya tidak banyak yang dapat Anda lakukan, ini adalah mekanisme internal dari jQuery. Tapi, hanya untuk tujuan pengujian, buka fungsinya dan ubah metode menjadi:

function cache( key, value ) {
    return value;
}

Sekarang jika Anda mengulangi prosesnya Anda tidak akan melihat simpul merah :)

Dokumentasi:

Jonathan Naguin
sumber
8
Saya menghargai usaha Anda. Memang, tiga teknik snapshot secara teratur disebutkan dalam tutorial. Sayangnya, detailnya sering diabaikan. Misalnya, saya menghargai pengenalan $0fungsi di konsol, yang baru bagi saya - tentu saja, saya tidak tahu apa yang sedang dilakukan atau bagaimana Anda tahu menggunakannya ( $1tampaknya tidak berguna sementara $2tampaknya melakukan hal yang sama). Kedua, bagaimana Anda tahu untuk menyorot baris #button in function cache()dan bukan dari puluhan baris lainnya? Akhirnya, ada simpul merah di dalam NodeListdan HTMLInputElementjuga, tapi saya tidak bisa mengetahuinya.
EleventyOne
7
Bagaimana Anda tahu bahwa cachebaris berisi informasi sementara yang lain tidak? Ada banyak cabang yang memiliki jarak lebih rendah dari yang cachesatu itu. Dan saya tidak yakin bagaimana Anda tahu bahwa itu HTMLInputElementadalah anak dari HTMLDivElement. Saya melihatnya direferensikan di dalamnya ("asli dalam HTMLDivElement"), tetapi juga referensi itu sendiri dan dua HTMLButtonElements, yang tidak masuk akal bagi saya. Saya tentu menghargai Anda mengidentifikasi jawaban untuk contoh ini, tetapi saya benar-benar tidak tahu bagaimana menggeneralisasikan ini ke masalah lain.
EleventyOne
2
Aneh, saya menggunakan contoh Anda dan saya mendapat hasil yang berbeda dari yang Anda lakukan (dari tangkapan layar Anda). Meskipun demikian, saya sangat menghargai semua bantuan Anda. Saya pikir saya sudah cukup untuk saat ini, dan ketika saya memiliki contoh kehidupan nyata yang saya perlukan bantuan khusus, saya akan membuat pertanyaan baru di sini. Terima kasih lagi.
EleventyOne
2
Penjelasan untuk $ 0 dapat ditemukan di sini: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
4
Apa Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.artinya
K - Keracunan dalam SO tumbuh.
8

Berikut tip pada profil memori jsfiddle: Gunakan URL berikut untuk mengisolasi hasil jsfiddle Anda, itu menghapus semua kerangka jsfiddle dan memuat hanya hasil Anda.

http://jsfiddle.net/4QhR2/show/

Saya tidak pernah dapat menemukan cara menggunakan Timeline dan Profiler untuk melacak kebocoran memori, sampai saya membaca dokumentasi berikut. Setelah membaca bagian yang berjudul 'Pelacak alokasi objek', saya dapat menggunakan alat 'Rekam Alokasi Tumpukan', dan melacak beberapa node DOM Terpisah.

Saya memperbaiki masalah dengan beralih dari pengikatan acara jQuery, ke menggunakan delegasi acara Backbone. Ini pemahaman saya bahwa versi baru dari Backbone akan secara otomatis melepaskan ikatan acara untuk Anda jika Anda menelepon View.remove(). Jalankan beberapa demo sendiri, mereka diatur dengan kebocoran memori untuk Anda identifikasi. Jangan ragu untuk bertanya di sini jika Anda masih belum mendapatkannya setelah mempelajari dokumentasi ini.

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling

Rick Suggs
sumber
6

Pada dasarnya Anda perlu melihat jumlah objek di dalam snapshot tumpukan Anda. Jika jumlah objek meningkat antara dua foto dan Anda telah membuang objek maka Anda memiliki kebocoran memori. Saran saya adalah untuk mencari penangan acara dalam kode Anda yang tidak terlepas.

Konstantin Dinev
sumber
3
Misalnya, jika saya melihat snapshot tumpukan jsfiddle, sebelum saya mengklik 'Hapus', ada lebih dari 100.000 objek hadir. Di mana saya akan mencari objek yang sebenarnya dibuat oleh kode jsfiddle saya? Saya pikir Window/http://jsfiddle.net/4QhR2/showmungkin bermanfaat, tapi itu hanya fungsi tanpa akhir. Saya tidak tahu apa yang terjadi di sana.
EleventyOne
@EleventyOne: Saya tidak akan menggunakan jsFiddle. Mengapa tidak membuat file di komputer Anda sendiri untuk pengujian?
Langit Biru
1
@ BlueSkies Saya membuat jsfiddle sehingga orang-orang di sini dapat bekerja dari basis kode yang sama. Namun demikian, ketika saya membuat file di komputer saya sendiri untuk pengujian, masih ada 50.000 objek hadir di snapshot tumpukan.
EleventyOne
@EleventyOne One heap snapshot tidak memberi Anda gagasan apakah Anda memiliki kebocoran memori atau tidak. Anda membutuhkan setidaknya dua.
Konstantin Dinev
2
Memang. Saya menyoroti betapa sulitnya untuk mengetahui apa yang harus dicari ketika ada ribuan objek hadir.
EleventyOne
3

Anda juga dapat melihat tab Timeline di alat pengembang. Catat penggunaan aplikasi Anda dan awasi hitungan DOM Node dan pendengar acara.

Jika grafik memori memang mengindikasikan kebocoran memori, maka Anda dapat menggunakan profiler untuk mencari tahu apa yang bocor.

Robert Falkén
sumber
2

Saya kedua saran untuk mengambil snapshot tumpukan, mereka sangat baik untuk mendeteksi kebocoran memori, chrome melakukan pekerjaan snapshotting yang sangat baik.

Dalam proyek penelitian saya untuk gelar saya, saya sedang membangun aplikasi web interaktif yang harus menghasilkan banyak data yang dibangun di 'lapisan', banyak dari lapisan ini akan 'dihapus' di UI tetapi untuk beberapa alasan memori tidak sedang deallocated, menggunakan alat snapshot saya dapat menentukan bahwa JQuery telah menyimpan referensi pada objek (sumbernya adalah ketika saya mencoba untuk memicu suatu .load()peristiwa yang menyimpan referensi meskipun keluar dari ruang lingkup). Memiliki informasi ini secara langsung menyelamatkan proyek saya, itu adalah alat yang sangat berguna ketika Anda menggunakan perpustakaan orang lain dan Anda memiliki masalah ini dengan referensi yang tersisa menghentikan GC dari melakukan pekerjaannya.

EDIT: Ini juga berguna untuk merencanakan ke depan tindakan apa yang akan Anda lakukan untuk meminimalkan waktu yang dihabiskan untuk snapshotting, berhipotesis apa yang dapat menyebabkan masalah dan uji setiap skenario, buat snapshot sebelum dan sesudah.

ProgrammerInProgress
sumber
0

Beberapa catatan penting dalam hal mengidentifikasi kebocoran memori menggunakan alat Pengembang Chrome:

1) Chrome sendiri memiliki kebocoran memori untuk elemen-elemen tertentu seperti kata sandi dan bidang angka. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Hindari menggunakan yang saat debugging karena mereka memoles snapshot tumpukan Anda ketika mencari elemen yang terpisah.

2) Hindari mencatat apa pun ke konsol browser. Chrome tidak akan membuang sampah mengumpulkan benda yang ditulis ke konsol, karenanya memengaruhi hasil Anda. Anda dapat menekan output dengan menempatkan kode berikut di awal skrip / halaman Anda:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3) Gunakan heap snapshots dan cari "detach" untuk mengidentifikasi elemen DOM yang terpisah. Dengan mengarahkan objek, Anda mendapatkan akses ke semua properti termasuk id dan outerHTML yang dapat membantu mengidentifikasi setiap elemen. Cuplikan layar JS Heap Snapshot dengan detail tentang elemen DOM terpisah Jika elemen yang terlepas masih terlalu umum untuk dikenali, tetapkan ID unik menggunakan konsol browser sebelum menjalankan tes Anda, misalnya:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

Sekarang, ketika Anda mengidentifikasi elemen yang terpisah dengan, katakanlah id = "AutoId_49", muat ulang halaman Anda, jalankan cuplikan di atas lagi, dan temukan elemen dengan id = "AutoId_49" menggunakan inspektur DOM atau document.querySelector (..) . Biasanya ini hanya berfungsi jika konten halaman Anda dapat diprediksi.

Bagaimana saya menjalankan tes saya untuk mengidentifikasi kebocoran memori

1) Muat halaman (dengan output konsol ditekan!)

2) Lakukan hal-hal pada halaman yang dapat menyebabkan kebocoran memori

3) Gunakan Alat Pengembang untuk mengambil snapshot tumpukan dan mencari "lepaskan"

4) Arahkan elemen untuk mengidentifikasi mereka dari properti id atau outerHTML mereka

Jimmy Thomsen
sumber
Juga, itu selalu merupakan ide yang baik untuk menonaktifkan minifying / uglifying karena membuat debugging di browser lebih sulit.
Jimmy Thomsen