Pembahasan mendalam tentang RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Saya Ian Kilpatrick, pemimpin teknik di tim tata letak Blink, bersama Koji Ishii. Sebelum bekerja di tim Blink, saya bekerja sebagai front-end engineer (sebelum Google berperan sebagai "front-end engineer"), yang membuat fitur di Google Dokumen, Drive, dan Gmail. Setelah sekitar lima tahun bekerja di sana, saya susah payah beralih ke tim Blink, secara efektif mempelajari C++ saat bekerja, dan mencoba mengembangkan codebase Blink yang sangat kompleks. Bahkan saat ini, saya hanya memahami sebagian kecil darinya. Saya bersyukur atas waktu yang diberikan kepada saya selama periode ini. Saya terhibur oleh fakta bahwa banyak "pemulihan teknisi front-end" melakukan transisi menjadi "insinyur browser" sebelum saya.

Pengalaman saya sebelumnya telah membimbing saya secara pribadi saat berada di tim Blink. Sebagai teknisi {i>front-end<i}, saya terus-menerus menghadapi inkonsistensi browser, masalah kinerja, bug rendering, dan fitur yang hilang. LayoutNG adalah kesempatan bagi saya untuk membantu memperbaiki masalah ini secara sistematis dalam sistem tata letak Blink, dan merepresentasikan hasil kerja banyak engineer selama bertahun-tahun.

Dalam postingan ini, saya akan menjelaskan bagaimana perubahan arsitektur besar seperti ini dapat mengurangi dan memitigasi berbagai jenis bug dan masalah performa.

Arsitektur layout engine setinggi 30.000 kaki

Sebelumnya, hierarki tata letak Blink adalah yang akan saya sebut sebagai "hierarki yang dapat diubah".

Menampilkan hierarki seperti yang dijelaskan dalam teks berikut.

Setiap objek dalam hierarki tata letak berisi informasi input, seperti ukuran tersedia yang diberlakukan oleh induk, posisi float, dan informasi output, misalnya, lebar dan tinggi akhir objek atau posisi x dan y-nya.

Objek ini disimpan di antara render. Saat terjadi perubahan gaya, kami menandai objek tersebut sebagai kotor dan juga semua induknya di pohon. Saat fase tata letak pipeline rendering dijalankan, kita akan membersihkan hierarki, memeriksa objek kotor, lalu menjalankan tata letak untuk membersihkannya.

Kami mendapati bahwa arsitektur ini mengakibatkan banyak kelas masalah, yang akan kami jelaskan di bawah ini. Tapi pertama-tama, mari kita mundur dan pertimbangkan input dan output tata letak.

Menjalankan tata letak pada node dalam hierarki ini secara konseptual mengambil "Gaya plus DOM", dan batasan induk apa pun dari sistem tata letak induk (petak, blok, atau fleksibel), menjalankan algoritme batasan tata letak, dan memberikan hasil.

Model konseptual yang dijelaskan sebelumnya.

Arsitektur baru kami merumuskan model konseptual ini. Kita masih memiliki hierarki tata letak, tetapi menggunakannya terutama untuk menyimpan input dan output tata letak. Untuk output, kita menghasilkan objek yang tidak dapat diubah dan benar-benar baru yang disebut hierarki fragmen.

Hierarki fragmen.

Saya telah membahas hierarki fragmen yang tidak dapat diubah sebelumnya, yang menjelaskan bagaimana hierarki tersebut dirancang untuk menggunakan kembali sebagian besar hierarki sebelumnya untuk tata letak inkremental.

Selain itu, kita menyimpan objek batasan induk yang menghasilkan fragmen tersebut. Kita menggunakannya sebagai kunci cache yang akan kita bahas lebih lanjut di bawah.

Algoritma tata letak inline (teks) juga ditulis ulang agar sesuai dengan arsitektur baru yang tidak dapat diubah. Library ini tidak hanya menghasilkan representasi daftar datar yang tidak dapat diubah untuk tata letak inline, tetapi juga dilengkapi caching tingkat paragraf untuk penataan ulang yang lebih cepat, bentuk-per-paragraf untuk menerapkan fitur font di seluruh elemen dan kata, algoritma dua arah Unicode baru menggunakan ICU, banyak perbaikan ketepatan, dan banyak lagi.

Jenis bug tata letak

Bug tata letak secara umum dikelompokkan ke dalam empat kategori berbeda, masing-masing memiliki penyebab utama yang berbeda.

Ketepatan

Saat memikirkan bug dalam sistem rendering, kami biasanya memikirkan ketepatan, misalnya: "Browser A memiliki perilaku X, sedangkan Browser B memiliki perilaku Y", atau "Browser A dan B rusak". Sebelumnya, kami menghabiskan banyak waktu dan terus-menerus berjuang dengan sistem. Mode kegagalan yang umum digunakan adalah menerapkan perbaikan yang ditargetkan untuk satu bug, tetapi beberapa minggu kemudian kami telah menyebabkan regresi di bagian sistem lain (yang tampaknya tidak terkait).

Seperti yang dijelaskan di postingan sebelumnya, ini adalah tanda sistem yang sangat rapuh. Khususnya untuk tata letak, kami tidak memiliki kontrak yang bersih di antara class, yang menyebabkan engineer browser bergantung pada status yang seharusnya tidak mereka lakukan, atau salah menafsirkan nilai dari bagian lain sistem.

Misalnya, pada satu titik kami memiliki rantai sekitar 10 bug selama lebih dari satu tahun, terkait dengan tata letak fleksibel. Setiap perbaikan menyebabkan masalah ketepatan atau performa di bagian sistem, sehingga mengakibatkan bug lain.

Karena LayoutNG dengan jelas mendefinisikan kontrak antara semua komponen dalam sistem tata letak, kami menemukan bahwa kami dapat menerapkan perubahan dengan jauh lebih yakin. Kami juga mendapatkan manfaat besar dari project Web Platform Tests (WPT) yang luar biasa, yang memungkinkan banyak pihak untuk berkontribusi pada rangkaian pengujian web umum.

Sekarang, kami mendapati bahwa jika merilis regresi nyata pada saluran stabil kami, biasanya regresi tersebut tidak memiliki pengujian terkait di repositori WPT, dan tidak disebabkan oleh kesalahpahaman tentang kontrak komponen. Selain itu, sebagai bagian dari kebijakan perbaikan bug kami, kami selalu menambahkan pengujian WPT baru, yang membantu memastikan bahwa tidak ada browser yang melakukan kesalahan yang sama.

Membatalkan validasi

Jika Anda pernah mengalami bug misterius ketika mengubah ukuran jendela browser atau mengalihkan properti CSS secara ajaib dapat menghilangkan bug, Anda akan mengalami masalah pembatalan validasi. Pada dasarnya, sebagian hierarki yang dapat diubah dianggap bersih, tetapi karena beberapa perubahan pada batasan induk, bagian tersebut tidak mewakili output yang benar.

Hal ini sangat umum pada mode tata letak dua jalur (berjalan struktur tata letak dua kali untuk menentukan status tata letak akhir) seperti yang dijelaskan di bawah ini. Sebelumnya, kode kita akan terlihat seperti ini:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

Perbaikan untuk jenis bug ini biasanya:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

Perbaikan untuk jenis masalah ini biasanya akan menyebabkan regresi performa yang parah, (lihat pembatalan validasi yang berlebihan di bawah), dan sulit untuk diperbaiki.

Saat ini (seperti yang dijelaskan di atas) kita memiliki objek batasan induk yang tidak dapat diubah yang menjelaskan semua input dari tata letak induk ke turunan. Kita menyimpannya dengan fragmen yang tidak dapat diubah yang dihasilkan. Oleh karena itu, kita memiliki tempat terpusat untuk membedakan kedua input ini untuk menentukan apakah turunan perlu melakukan penerusan tata letak lain. Logika diffing ini rumit, tetapi terdokumentasi dengan baik. Men-debug class masalah yang belum divalidasi ini biasanya menyebabkan pemeriksaan dua input secara manual dan memutuskan perubahan apa dalam input sehingga penerusan tata letak lain diperlukan.

Perbaikan pada kode diffing ini biasanya sederhana dan dapat diuji unit dengan mudah karena pembuatan objek independen ini sederhana.

Membandingkan gambar lebar dan persentase lebar yang tetap.
Elemen lebar/tinggi tetap tidak peduli jika ukuran yang tersedia yang diberikan meningkat, tetapi lebar/tinggi berbasis persentase memengaruhinya. Available-size direpresentasikan pada objek Parent Constraints, dan sebagai bagian dari algoritma diffing akan melakukan pengoptimalan ini.

Kode diffing untuk contoh di atas adalah:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

Histeresis

Jenis bug ini serupa dengan pembatalan validasi. Pada dasarnya, di sistem sebelumnya sangat sulit untuk memastikan bahwa tata letak bersifat idempoten-yaitu, menjalankan ulang tata letak dengan input yang sama, menghasilkan output yang sama.

Pada contoh di bawah, kita hanya mengalihkan properti CSS bolak-balik di antara dua nilai. Namun, hal ini menghasilkan persegi panjang yang "bertambah tanpa batas".

Video dan demo menampilkan bug hysteresis di Chrome 92 dan versi yang lebih lama. Masalah ini telah diperbaiki di Chrome 93.

Pada hierarki yang dapat berubah sebelumnya, sangat mudah menemukan bug seperti ini. Jika kode keliru membaca ukuran atau posisi objek pada waktu atau tahapan yang salah (misalnya, karena kita tidak "menghapus" ukuran atau posisi sebelumnya), kami akan segera menambahkan bug Histeresis yang halus. Bug ini biasanya tidak muncul dalam pengujian karena sebagian besar pengujian berfokus pada tata letak dan render tunggal. Yang lebih mengkhawatirkan, kami tahu bahwa beberapa hysteresis ini diperlukan agar beberapa mode tata letak berfungsi dengan benar. Kami memiliki bug saat melakukan pengoptimalan untuk menghapus penerusan tata letak, tetapi memperkenalkan "bug" karena mode tata letak memerlukan dua penerusan untuk mendapatkan output yang benar.

Pohon yang mendemonstrasikan masalah-masalah yang dijelaskan dalam teks sebelumnya.
Bergantung pada informasi hasil tata letak sebelumnya, menghasilkan tata letak non-idempoten

Dengan LayoutNG, karena kami memiliki struktur data input dan output eksplisit, dan mengakses status sebelumnya tidak diizinkan, kami telah secara luas memitigasi kelas bug ini dari sistem tata letak.

Pembatalan validasi dan performa yang berlebihan

Hal ini adalah kebalikan langsung dari class bug yang sedang dibatalkan validasinya. Sering kali saat memperbaiki bug yang berada di bawah pembatalan validasi, kami dapat memicu gangguan performa.

Kami sering kali harus membuat pilihan sulit yang mengutamakan ketepatan daripada performa. Di bagian berikutnya, kita akan mempelajari lebih dalam cara memitigasi jenis masalah performa ini.

Naiknya tata letak dua jalur dan tebing performa

Tata letak fleksibel dan petak mewakili pergeseran ekspresi tata letak di web. Namun, algoritma ini pada dasarnya berbeda dari algoritma tata letak blok yang ada sebelumnya.

Tata letak blok (di hampir semua kasus) hanya memerlukan mesin untuk melakukan tata letak pada semua turunannya tepat satu kali. Ini bagus untuk performa, tetapi pada akhirnya tidak seekspresif yang diinginkan developer web.

Misalnya, sering kali Anda ingin ukuran semua turunan diperluas ke ukuran yang terbesar. Untuk mendukung hal ini, tata letak induk (fleksi dan petak) akan melakukan tahap pengukuran untuk menentukan ukuran setiap turunan, lalu penerusan tata letak untuk merentangkan semua turunan ke ukuran ini. Perilaku ini adalah default untuk tata letak fleksibel dan petak.

Dua kumpulan kotak, yang pertama menunjukkan ukuran intrinsik kotak dalam tahap pengukuran, dan kotak kedua berada pada tata letak dengan ketinggian yang sama.

Tata letak dua jalur ini awalnya dapat diterima dari segi performa, karena orang biasanya tidak menyusunnya secara mendalam. Namun, kami mulai melihat masalah performa yang signifikan seiring munculnya konten yang lebih rumit. Jika Anda tidak meng-cache hasil fase pengukuran, hierarki tata letak akan beralih antara status pengukuran dan status tata letak terakhirnya.

Tata letak satu, dua, dan tiga jalur yang dijelaskan dalam teks.
Pada gambar di atas, kita memiliki tiga elemen <div>. Tata letak satu jalur sederhana (seperti tata letak blok) akan mengunjungi tiga node tata letak (kompleksitas O(n)). Namun, untuk tata letak dua jalur (seperti fleksibel atau petak), hal ini dapat berpotensi menyebabkan kompleksitas O(2n) kunjungan untuk contoh ini.
Grafik yang menunjukkan peningkatan eksponensial waktu tata letak.
Gambar dan demo ini menampilkan tata letak eksponensial dengan tata letak Petak. Hal ini telah diperbaiki di Chrome 93 sebagai akibat dari pemindahan Grid ke arsitektur baru

Sebelumnya, kita akan mencoba menambahkan cache yang sangat spesifik ke tata letak fleksibel dan petak untuk mengatasi jenis tebing performa ini. Cara ini berhasil (dan kami terus berkembang dengan Flex), tetapi kami terus berkelahi dengan bug pembatalan validasi yang terjadi secara terus-menerus.

LayoutNG memungkinkan kita membuat struktur data eksplisit untuk input dan output tata letak, dan selain itu kita telah membangun cache penerusan ukuran dan tata letak. Ini membawa kompleksitas kembali ke O(n), sehingga menghasilkan performa linear yang bisa diprediksi bagi developer web. Jika ada kasus di mana tata letak melakukan tata letak tiga penerusan, kita juga akan meng-cache itu yang lewat. Hal ini dapat membuka peluang untuk memperkenalkan mode tata letak lebih lanjut dengan aman dalam contoh bagaimana RenderingNG pada dasarnya membuka ekstensibilitas secara menyeluruh. Dalam beberapa kasus, tata letak Petak dapat memerlukan tata letak tiga jalur, tetapi sangat jarang untuk saat ini.

Kami menemukan bahwa saat developer mengalami masalah performa khususnya dengan tata letak, ini biasanya disebabkan oleh bug waktu tata letak eksponensial, bukan throughput mentah tahap tata letak pipeline. Jika perubahan inkremental kecil (satu elemen mengubah satu properti CSS) menghasilkan tata letak 50-100 md, ini kemungkinan adalah bug tata letak eksponensial.

Ringkasan

Tata letak adalah area yang sangat kompleks, dan kami tidak membahas semua detail menarik seperti pengoptimalan tata letak inline (sebenarnya cara kerja seluruh subsistem teks dan inline), dan bahkan konsep yang dibahas di sini hanya sebagian kecil, dan mengabaikan banyak detail. Namun, semoga kita telah menunjukkan bagaimana meningkatkan arsitektur suatu sistem secara sistematis dapat menghasilkan keuntungan yang besar dalam jangka panjang.

Meskipun demikian, kami sadar bahwa masih banyak pekerjaan yang harus kami lakukan. Kami mengetahui kelas masalah (baik performa maupun ketepatan) yang sedang kami selesaikan, dan kami sangat antusias dengan fitur tata letak baru yang akan hadir di CSS. Kami yakin arsitektur LayoutNG membuat penyelesaian masalah ini dengan aman dan dapat dilakukan.

Satu gambar (Anda tahu yang mana!) oleh Una Kravets.