Pembahasan mendalam tentang RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Saya Ian Kilpatrick, kepala engineering di tim tata letak Blink, bersama Koji Ishii. Sebelum bekerja di tim Blink, Saya dulunya adalah seorang insinyur {i>front-end<i} (sebelum Google memiliki peran "{i>front-end engineer<i}"), membuat fitur dalam Google Dokumen, Drive, dan Gmail. Setelah sekitar lima tahun di posisi itu, saya mengambil risiko besar untuk beralih ke tim Blink, mempelajari C++ secara efektif saat bekerja, dan berupaya meningkatkan codebase Blink yang sangat kompleks. Bahkan saat ini, saya hanya memahami sebagian kecil saja. Saya berterima kasih atas waktu yang diberikan kepada saya selama periode ini. Saya merasa nyaman dengan fakta bahwa banyak "memulihkan insinyur {i>front-end<i}" beralih menjadi "insinyur browser" sebelum saya.

Pengalaman saya sebelumnya telah membimbing saya secara pribadi selama berada di tim Blink. Sebagai insinyur {i>front-end<i} saya terus-menerus mengalami inkonsistensi {i>browser<i}, masalah kinerja, {i>bug<i} rendering, dan fitur yang hilang. LayoutNG memberikan kesempatan bagi saya untuk membantu memperbaiki masalah ini secara sistematis dalam sistem tata letak Blink, yang mewakili jumlah jumlah insinyur dari tahun ke tahun.

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

Tampilan 30.000 kaki dari tata letak arsitektur mesin

Sebelumnya, struktur hierarki Blink akan saya sebut sebagai "pohon yang dapat berubah".

Menampilkan hierarki seperti yang dijelaskan dalam teks berikut.

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

Objek ini disimpan di antara render. Ketika terjadi perubahan gaya, kita menandai objek itu sebagai kotor dan semua induknya di pohon. Ketika fase tata letak dari pipeline rendering dijalankan, kita akan membersihkan pohon, menelusuri objek kotor, lalu menjalankan tata letak untuk mengembalikan ke kondisi bersih.

Kami menemukan bahwa arsitektur ini menyebabkan banyak kelas masalah, yang akan kami jelaskan di bawah ini. Tapi pertama-tama, mari kita mundur ke belakang dan pertimbangkan apa itu {i>input<i} dan {i>output<i} dari tata letak.

Menjalankan tata letak pada simpul di pohon ini secara konseptual menggunakan "Style plus DOM", dan semua batasan induk dari sistem tata letak induk (grid, block, atau flex), menjalankan algoritma batasan tata letak, dan memberikan hasil.

Model konseptual yang telah 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 membuat objek tidak dapat diubah yang benar-benar baru, yang disebut hierarki fragmen.

Hierarki fragmen.

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

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

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

Jenis bug tata letak

{i>Bug tata letak<i} secara umum masuk ke dalam empat kategori yang berbeda, masing-masing dengan akar penyebab yang berbeda.

Ketepatan

Ketika kita berpikir tentang {i>bug<i} dalam sistem {i>rendering<i}, kita biasanya memikirkan tentang ketepatan, misalnya: "Browser A memiliki perilaku X, sedangkan Browser B memiliki perilaku Y", atau "Browser A dan B rusak". Sebelumnya kita telah menghabiskan banyak waktu, dan dalam prosesnya, kami terus-menerus berurusan dengan sistem. Mode kegagalan yang umum adalah menerapkan perbaikan yang ditargetkan untuk satu {i>bug<i}, tetapi berminggu-minggu kemudian, ditemukan bahwa kita telah menyebabkan regresi di bagian sistem yang lain (yang sepertinya tidak terkait).

Seperti yang dijelaskan dalam postingan sebelumnya, ini adalah tanda bahwa sistem itu sangat rapuh. Khususnya untuk tata letak, kita tidak memiliki kontrak yang bersih antara kelas mana pun, menyebabkan insinyur browser bergantung pada status yang tidak seharusnya, atau salah menginterpretasikan beberapa nilai dari bagian lain sistem.

Sebagai contoh, pada satu titik kami memiliki rantai sekitar 10 bug selama lebih dari satu tahun, yang berkaitan dengan tata letak fleksibel. Setiap perbaikan menyebabkan masalah kebenaran atau kinerja di bagian sistem, yang menyebabkan {i> bug<i} lagi.

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

Sekarang kita menemukan bahwa jika kita merilis regresi yang sebenarnya di saluran stabil, biasanya tidak memiliki tes terkait di repositori WPT, dan bukan akibat dari kesalahpahaman tentang kontrak komponen. Selanjutnya, sebagai bagian dari kebijakan perbaikan {i>bug<i}, kami selalu menambahkan pengujian membantu memastikan bahwa tidak ada {i>browser<i} yang seharusnya melakukan kesalahan yang sama.

Dalam pembatalan validasi

Jika Anda pernah mengalami {i>bug<i} misterius di mana mengubah ukuran jendela {i>browser<i} atau mengganti properti CSS secara ajaib dapat menghilangkan {i>bug<i} tersebut, Anda mengalami masalah di bawah pembatalan validasi. Secara efektif, sebagian pohon yang dapat berubah dianggap bersih, tetapi karena beberapa perubahan pada batasan induk, output tersebut tidak benar.

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

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

Perbaikan untuk jenis bug ini biasanya adalah:

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

Perbaikan untuk jenis masalah ini biasanya akan menyebabkan regresi kinerja yang parah, (lihat pembatalan validasi di bawah), dan sangat sulit untuk mendapatkan jawaban yang benar.

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 menyimpan ini dengan fragmen yang tidak dapat diubah. Karena ini, kita memiliki tempat terpusat untuk membedakan kedua input ini untuk menentukan apakah turunan perlu melakukan penerusan tata letak lain. Logika {i>diffing <i}ini rumit, tetapi tersimpan dengan baik. Proses debug class masalah kekurangan validasi ini biasanya mengakibatkan pemeriksaan kedua input secara manual dan memutuskan apa yang berubah dalam input sehingga diperlukan penerusan tata letak lain.

Perbaikan pada kode {i>diffing<i} ini biasanya sederhana, dan mudah diuji unit karena kemudahan pembuatan objek independen ini.

Membandingkan gambar dengan lebar tetap dan lebar persentase.
Elemen lebar/tinggi tetap tidak peduli apakah ukuran yang tersedia yang diberikan untuknya meningkat, tetapi lebar/tinggi berbasis persentase meningkat. available-size akan ditampilkan pada objek available-size dan pengoptimalan ini akan dilakukan sebagai bagian dari algoritma diffing.

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

Kelas bug ini mirip dengan kekurangan validasi. Pada dasarnya, dalam 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 ini, kita hanya mengganti properti CSS secara bolak-balik di antara dua nilai. Namun, hal ini menghasilkan "peningkatan tanpa batas" persegi panjang.

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

Pada pohon yang sebelumnya dapat berubah, sangat mudah untuk memperkenalkan {i>bug<i} seperti ini. Jika kode membuat kesalahan dalam membaca ukuran atau posisi objek pada waktu atau tahap yang salah (karena kita tidak "menghapus" ukuran atau posisi sebelumnya), kita akan segera menambahkan {i> bug hysteresis<i} yang halus. Bug ini biasanya tidak muncul dalam pengujian karena sebagian besar pengujian berfokus pada satu tata letak dan render. Yang lebih mengkhawatirkan, kami mengetahui bahwa beberapa hipotesis ini diperlukan agar beberapa mode tata letak berfungsi dengan benar. Kami memiliki {i>bug<i} di mana kami harus melakukan pengoptimalan untuk menghapus {i>layout pass<i}, tapi memperkenalkan "bug" karena mode tata letak memerlukan dua penerusan untuk mendapatkan {i>output <i}yang benar.

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

Dengan LayoutNG, karena kita memiliki struktur data input dan output yang 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 kebalikan langsung dari kelas bug yang sedang dalam pembatalan validasi. Sering kali saat memperbaiki bug yang kurang validasi, kami akan memicu tebing performa.

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

Bangkitnya tata letak dua tahap dan tebing performa

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

Tata letak blok (dalam hampir semua kasus) hanya memerlukan mesin untuk melakukan tata letak pada semua turunannya tepat satu kali. Cara ini bagus untuk performa, tetapi hasilnya tidak sejelas yang diinginkan developer web.

Misalnya, sering kali Anda ingin ukuran semua turunan bertambah menjadi ukuran terbesar. Untuk mendukung hal ini, tata letak induk (fleksibel atau petak) akan melakukan pengukuran untuk menentukan seberapa besar setiap turunan, kemudian penerusan tata letak untuk merentangkan semua turunan ke ukuran ini. Perilaku ini adalah setelan default untuk tata letak fleksibel dan petak.

Dua set kotak, yang pertama menunjukkan ukuran intrinsik kotak dalam tahap pengukuran, yang kedua pada tata letak dengan tinggi yang sama.

Tata letak dua tahap ini awalnya dapat diterima dari segi kinerja, karena orang-orang biasanya tidak menyusunnya secara mendalam. Namun, kami mulai melihat masalah performa yang signifikan saat muncul konten yang lebih kompleks. Jika Anda tidak meng-cache hasil fase pengukuran, struktur tata letak akan berpindah antara status measure, dan status layout akhirnya.

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

Sebelumnya, kita akan mencoba menambahkan cache yang sangat spesifik untuk menampilkan tata letak fleksibel dan petak untuk mengatasi jenis tebing performa ini. Cara ini berhasil (dan kita sudah sangat jauh dengan Flex), tetapi terus-menerus berurusan dengan bug pembatalan validasi yang berlebihan.

LayoutNG memungkinkan kita membuat struktur data eksplisit baik untuk input maupun output tata letak, dan yang lebih penting, kita telah membuat cache pengukuran dan tata letak. Ini membawa kompleksitas kembali ke O(n), yang menghasilkan kinerja linier yang dapat diprediksi untuk pengembang web. Jika ada kasus di mana tata letak melakukan tata letak tiga tahap, kita juga akan menyimpan cache yang lulus tersebut. Hal ini dapat membuka peluang untuk secara aman memperkenalkan mode tata letak yang lebih canggih pada masa mendatang. Ini adalah contoh cara RenderingNG pada dasarnya membuka ekstensibilitas secara menyeluruh. Dalam beberapa kasus, tata letak Petak dapat memerlukan tata letak tiga tahap, tetapi sangat jarang terjadi pada saat ini.

Kami menemukan bahwa ketika pengembang mengalami masalah kinerja khususnya terkait tata letak, hal ini biasanya disebabkan oleh bug waktu tata letak eksponensial, bukan throughput mentah dari tahap tata letak pipeline. Jika perubahan inkremental kecil (satu elemen mengubah satu properti CSS) menghasilkan tata letak 50-100 md, kemungkinan ini adalah {i>bug<i} tata letak eksponensial.

Ringkasan

Tata letak adalah area yang sangat kompleks, dan kita tidak membahas semua detail yang menarik seperti pengoptimalan tata letak sebaris (bagaimana seluruh sub-sistem inline dan teks bekerja), dan bahkan konsep yang dibicarakan di sini benar-benar baru sebagian kecilnya, dan mengabaikan banyak detail. Namun, semoga kita telah menunjukkan betapa sistematisnya peningkatan arsitektur sistem dalam jangka panjang dapat memberikan keuntungan yang besar.

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

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