Peramban modern adalah proyek yang sangat kompleks di mana bahkan perubahan yang tampak tidak berbahaya dapat menyebabkan kejutan yang tak terduga. Oleh karena itu, ada banyak tes internal yang harus menangkap perubahan tersebut sebelum dirilis. Tidak pernah ada terlalu banyak tes, jadi berguna juga untuk menggunakan tolok ukur publik pihak ketiga.
Nama saya Andrey Logvinov, saya bekerja di grup pengembangan mesin rendering Yandex.Browser di Nizhny Novgorod. Hari ini saya akan memberi tahu pembaca Habr tentang bagaimana manajemen memori dalam proyek Chromium bekerja dengan contoh satu masalah misterius yang menyebabkan penurunan kinerja dalam tes
Speedometer . Posting ini berdasarkan laporan saya dari acara Yandex.Inside.

Begitu berada di dashboard kinerja kami, kami melihat penurunan dalam kecepatan tes Speedometer. Tes ini mengukur keseluruhan kinerja browser pada aplikasi yang mendekati kenyataan - daftar tugas, di mana tes menambahkan item ke daftar dan kemudian mencoretnya. Hasil pengujian dipengaruhi oleh kinerja mesin V8 JS dan kecepatan rendering halaman di mesin Blink. Tes Speedometer terdiri dari beberapa subyek, di mana aplikasi pengujian ditulis menggunakan salah satu kerangka kerja JS yang populer, misalnya jQuery atau ReactJS. Hasil tes keseluruhan didefinisikan sebagai rata-rata untuk hasil untuk semua kerangka kerja, tetapi tes ini memungkinkan Anda untuk melihat kinerja untuk setiap kerangka kerja secara individual. Perlu dicatat bahwa tes ini tidak bertujuan untuk mengevaluasi kinerja kerangka kerja, mereka hanya digunakan untuk membuat tes kurang sintetis dan lebih dekat dengan aplikasi web nyata. Detailing oleh subtest menunjukkan bahwa penurunan diamati hanya untuk versi aplikasi uji yang dibuat menggunakan jQuery. Dan ini sudah menarik, setujui.
Investigasi situasi semacam itu dimulai dengan cukup standar - kami menentukan komit tertentu yang mengarah pada masalah. Untuk melakukan ini, kami menyimpan rakitan Yandex.Browser untuk masing-masing (!) Berkomitmen selama beberapa tahun terakhir (akan sulit untuk dirakit kembali, karena perakitan membutuhkan waktu beberapa jam). Ini membutuhkan banyak ruang di server, tetapi biasanya membantu untuk dengan cepat menemukan sumber masalahnya. Tetapi kali ini dengan cepat tidak berhasil. Ternyata kemunduran hasil tes bertepatan dengan komitmen mengintegrasikan versi Chromium berikutnya. Hasilnya tidak menggembirakan, karena versi baru Chromium membawa banyak perubahan sekaligus.
Karena kami tidak menerima informasi apa pun yang mengindikasikan perubahan tertentu, saya harus melakukan studi substantif terhadap masalah tersebut. Untuk melakukan ini, kami menggunakan Alat Pengembang untuk menghapus jejak pengujian. Kami memperhatikan fitur aneh - interval "sobek" untuk eksekusi fungsi uji Javascript.

Kami menghapus jejak yang lebih teknis dengan tentang: melacak dan melihat bahwa itu adalah
pengumpulan sampah (GC) di Blink.

Jalur memori di bawah ini menunjukkan bahwa jeda-GC ini tidak hanya menghabiskan banyak waktu, tetapi juga tidak membantu menghentikan pertumbuhan konsumsi memori.

Tetapi jika Anda memasukkan panggilan GC eksplisit ke dalam tes, maka kami melihat gambar yang sama sekali berbeda - memori disimpan di wilayah nol dan tidak bocor. Jadi, kami tidak memiliki kebocoran memori, dan masalahnya terkait dengan fitur kolektor. Kami terus menggali. Kami memulai debugger dan melihat bahwa pemulung telah melewati sekitar 500 ribu objek! Sejumlah objek seperti itu tidak dapat memengaruhi kinerja. Tapi dari mana asalnya?
Dan di sini kita perlu kilas balik kecil tentang perangkat pengumpul sampah di Blink. Ini menghapus objek mati, tetapi tidak memindahkan objek hidup, yang memungkinkan operasi dengan pointer kosong dalam variabel lokal dalam kode C ++. Pola ini aktif digunakan dalam Blink. Tetapi ia juga memiliki harganya - ketika mengumpulkan sampah, Anda harus
memindai tumpukan aliran, dan jika sesuatu yang mirip dengan penunjuk ke objek dari tumpukan (tumpukan) ditemukan di sana, maka pertimbangkan objek dan segala sesuatu yang merujuk langsung atau tidak langsung menjadi hidup. Ini mengarah pada fakta bahwa beberapa benda yang secara virtual tidak dapat diakses dan karenanya "mati" diidentifikasi sebagai benda hidup. Oleh karena itu, bentuk pengumpulan sampah ini juga disebut konservatif.
Kami memeriksa koneksi dengan pemindaian tumpukan dan melewatkannya. Masalahnya telah hilang.
Apa yang bisa seperti itu dalam tumpukan yang menampung 500 ribu objek? Kami menempatkan breakpoint dalam fungsi menambahkan objek - antara lain, kami melihat ada yang mencurigakan:
blink :: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...
Referensi tabel hash kemungkinan adalah tersangka! Kami menguji hipotesis dengan melewatkan penambahan tautan ini. Masalahnya telah hilang. Baiklah, kita selangkah lebih dekat ke jawabannya.
Kami mengingat fitur lain dari pengumpul sampah di Blink: jika ia melihat pointer ke bagian dalam tabel hash, maka itu menganggap ini sebagai tanda iterasi yang sedang berlangsung di atas meja, yang berarti bahwa itu menganggap semua tautan di tabel ini berguna dan terus memintasinya. Dalam kasus kami, idle. Tetapi fungsi apa yang menjadi sumber tautan ini?
Kami memajukan beberapa bingkai tumpukan lebih tinggi, mengambil posisi pemindai saat ini, melihat bingkai tumpukan yang fungsinya jatuh ke dalamnya. Ini adalah fungsi yang disebut
ScheduleGCIfNeeded . Tampaknya di sini dia pelakunya, tapi ... kita melihat kode sumber fungsi dan melihat bahwa tidak ada tabel hash sama sekali. Selain itu, ini sudah menjadi bagian dari pengumpul sampah itu sendiri, dan itu tidak perlu merujuk ke objek dari tumpukan Blink. Dari mana tautan "buruk" ini berasal?
Kami menetapkan breakpoint untuk mengubah sel memori, di mana kami menemukan tautan ke tabel hash. Kita melihat bahwa salah satu fungsi internal yang disebut V8PerIsolateData :: AddActiveScriptWrappable menulis di sana. Di sana, mereka menambahkan beberapa elemen HTML yang dibuat dari beberapa jenis, termasuk input, ke tabel hash active_script_wrappables_ tunggal. Tabel ini diperlukan untuk mencegah penghapusan elemen yang tidak lagi dirujuk dari Javascript atau pohon DOM, tetapi yang terkait dengan aktivitas eksternal yang, misalnya, dapat menghasilkan peristiwa.
Pengumpul sampah selama melintasi tabel normal memperhitungkan status elemen yang terkandung di dalamnya dan menandainya sebagai hidup atau tidak menandainya, kemudian dihapus pada tahap perakitan berikutnya. Namun, dalam kasus kami, sebuah penunjuk ke penyimpanan internal tabel ini muncul ketika tumpukan dipindai, dan semua elemen tabel ditandai sebagai langsung.
Tetapi bagaimana nilai dari tumpukan satu fungsi mencapai tumpukan lainnya?!
Pikirkan ScheduleGCI yang Diperlukan. Ingatlah bahwa tidak ada yang berguna yang ditemukan dalam kode sumber fungsi ini, tetapi ini hanya berarti bahwa ini saatnya untuk turun ke level yang lebih rendah dan memeriksa
kompiler . Prolog yang dibongkar dari fungsi ScheduleGCIfNeeded terlihat seperti ini:
0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax
Dapat dilihat bahwa fungsi
bergerak esp ke 0B8h , dan tempat ini tidak digunakan lebih lanjut. Tetapi karena ini, pemindai tumpukan melihat apa yang sebelumnya direkam oleh fungsi lain. Dan kebetulan, sebuah pointer ke bagian dalam tabel hash yang ditinggalkan oleh fungsi AddActiveScriptWrappable masuk ke "lubang" ini. Ternyata, alasan munculnya "lubang" dalam kasus ini adalah
makro debug
VLOG di dalam fungsi, yang menampilkan informasi tambahan dalam log.
Tetapi mengapa tabel active_script_wrappable_ memiliki ratusan ribu elemen? Mengapa penurunan kinerja hanya diamati pada tes jQuery? Jawaban untuk kedua pertanyaan itu sama - dalam pengujian khusus ini, untuk setiap perubahan (seperti tanda centang di kotak centang) seluruh UI dibuat ulang sepenuhnya. Tes menghasilkan elemen-elemen yang segera berubah menjadi sampah. Sisa tes dalam Speedometer lebih bijaksana dan tidak membuat elemen yang tidak perlu, oleh karena itu, penurunan kinerja tidak diamati untuk mereka. Jika Anda mengembangkan layanan web, maka Anda harus mempertimbangkan ini agar tidak membuat pekerjaan yang tidak perlu untuk browser.
Tetapi mengapa masalah baru muncul sekarang jika VLOG makro sebelumnya? Tidak ada jawaban yang pasti, tetapi kemungkinan besar, selama pembaruan, posisi relatif elemen-elemen pada stack berubah, karena itu penunjuk ke tabel hash secara tidak sengaja dapat diakses oleh pemindai. Faktanya, kami memenangkan lotre. Untuk segera menutup "lubang" dan mengembalikan kinerja, kami menghapus makro debug VLOG. Untuk pengguna, ini tidak berguna, dan untuk kebutuhan diagnostik kami sendiri, kami selalu dapat mengaktifkannya kembali. Kami juga berbagi pengalaman kami dengan pengembang Chromium lainnya. Jawabannya mengkonfirmasi ketakutan kami: ini adalah masalah mendasar pengumpulan sampah konservatif di Blink, yang tidak memiliki solusi sistemik.
Tautan menarik
1. Jika Anda tertarik mempelajari tentang kehidupan sehari-hari kelompok kami yang tidak biasa, ingat
kisah tentang persegi panjang hitam , yang mengarah pada percepatan tidak hanya Yandex.Browser, tetapi seluruh proyek Chromium.
2. Dan saya juga mengundang Anda untuk mendengarkan laporan lain di acara
Yandex berikutnya.
Di dalam acara pada 16 Februari, pendaftaran terbuka, dan siarannya juga akan.