Pembahasan mendalam tentang RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Saya Ian Kilpatrick, pemimpin engineering di tim tata letak Blink, bersama dengan Koji Ishii. Sebelum bekerja di tim Blink, saya adalah engineer frontend (sebelum Google memiliki peran "engineer frontend"), yang membuat fitur dalam Google Dokumen, Drive, dan Gmail. Setelah sekitar lima tahun menjalankan peran itu, saya mengambil risiko besar untuk beralih ke tim Blink, secara efektif mempelajari C++ saat bekerja, dan mencoba meningkatkan codebase Blink yang sangat kompleks. Bahkan hingga saat ini, saya hanya memahami sebagian kecil darinya. Saya berterima kasih atas waktu yang diberikan kepada saya selama periode ini. Saya merasa terhibur dengan kenyataan bahwa banyak "engineer front-end yang sedang pulih" melakukan transisi menjadi "engineer browser" sebelum saya.

Pengalaman saya sebelumnya telah membimbing saya secara pribadi saat berada di tim Blink. Sebagai engineer frontend, saya terus-menerus mengalami inkonsistensi browser, masalah performa, bug rendering, dan fitur yang tidak ada. LayoutNG adalah peluang bagi saya untuk membantu memperbaiki masalah ini secara sistematis dalam sistem tata letak Blink, dan mewakili jumlah upaya 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.

Tampilan arsitektur mesin tata letak dari ketinggian 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 yang tersedia yang diterapkan 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 itu kotor dan semua induknya di pohon. Saat fase tata letak dari pipeline rendering dijalankan, kita akan membersihkan hierarki, menjalankan objek kotor, lalu menjalankan tata letak untuk membuatnya bersih.

Kami mendapati bahwa arsitektur ini menghasilkan banyak class masalah, yang akan kami jelaskan di bawah. Namun, pertama-tama, mari kita mundur sejenak dan pertimbangkan input dan output tata letak.

Menjalankan tata letak pada node di hierarki ini secara konseptual menggunakan "Gaya plus DOM", dan batasan induk apa pun dari sistem tata letak induk (petak, blok, atau fleksibel), menjalankan algoritme batasan tata letak, dan menghasilkan 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 membuat objek yang tidak dapat diubah yang sama sekali baru yang disebut hierarki fragmen.

Hierarki fragmen.

Saya telah membahas hierarki fragmen yang tidak dapat diubah sebelumnya, yang menjelaskan cara 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 cocok dengan arsitektur baru yang tidak dapat diubah. Produk ini tidak hanya menghasilkan representasi daftar datar yang tidak dapat diubah untuk tata letak sebaris, tetapi juga dilengkapi caching tingkat paragraf untuk tata letak ulang yang lebih cepat, bentuk-per-paragraf untuk menerapkan fitur font di seluruh elemen dan kata, algoritma dua arah Unicode baru yang menggunakan ICU, banyak perbaikan ketepatan, dan banyak lagi.

Jenis bug tata letak

Secara umum, bug tata letak terbagi menjadi empat kategori berbeda, masing-masing dengan 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, inilah yang menghabiskan banyak waktu kita, dan dalam prosesnya, kita terus-menerus berurusan dengan sistem. Mode kegagalan yang umum adalah menerapkan perbaikan yang sangat ditargetkan untuk satu bug, tetapi beberapa minggu kemudian kami mendapati bahwa kami telah menyebabkan regresi di bagian lain sistem (yang tampaknya tidak terkait).

Seperti yang dijelaskan dalam postingan sebelumnya, hal ini adalah tanda sistem yang sangat rapuh. Khusus untuk tata letak, kami tidak memiliki kontrak yang jelas antara class apa pun, sehingga menyebabkan engineer browser bergantung pada status yang tidak seharusnya, atau salah menafsirkan beberapa nilai dari bagian lain sistem.

Sebagai contoh, pada satu titik kami memiliki rangkaian sekitar 10 bug selama lebih dari setahun, terkait tata letak fleksibel. Setiap perbaikan menyebabkan masalah akurasi atau performa di bagian sistem, yang menyebabkan bug lainnya.

Setelah LayoutNG menentukan kontrak antara semua komponen dalam sistem tata letak dengan jelas, kami mendapati bahwa kita dapat menerapkan perubahan dengan lebih yakin. Kami juga mendapatkan manfaat besar dari project Web Platform Tests (WPT) yang sangat baik, yang memungkinkan banyak pihak untuk berkontribusi pada rangkaian pengujian web umum.

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

Pembatalan validasi

Jika pernah mengalami bug misterius yang membuat bug hilang secara ajaib saat mengubah ukuran jendela browser atau mengalihkan properti CSS, Anda mengalami masalah pembatalan validasi yang tidak memadai. Secara efektif, sebagian hierarki yang dapat diubah dianggap bersih, tetapi karena beberapa perubahan pada batasan induk, hierarki tersebut tidak merepresentasikan output yang benar.

Hal ini sangat umum pada mode tata letak dua tahap (berjalan dua kali hierarki tata letak untuk menentukan status tata letak akhir) 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 performa yang parah, (lihat pembatalan validasi berlebih di bawah), dan sangat 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. Karena itu, kami memiliki tempat terpusat untuk membedakan kedua input ini guna menentukan apakah turunan perlu melakukan penerusan tata letak lain. Logika perbedaan ini rumit, tetapi terkandung dengan baik. Proses debug class masalah kekurangan validasi ini biasanya mengakibatkan pemeriksaan kedua input secara manual dan memutuskan hal apa yang berubah dalam input sehingga diperlukan penerusan tata letak lain.

Perbaikan pada kode perbedaan ini biasanya sederhana, dan mudah diuji unit karena kemudahan pembuatan objek independen ini.

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

Kode perbedaan 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;
}

Histerisis

Jenis bug ini mirip dengan pembatalan validasi yang tidak memadai. 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.

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

Video dan demo menunjukkan bug histeresis di Chrome 92 dan yang lebih lama. Masalah ini telah diperbaiki di Chrome 93.

Dengan hierarki yang dapat diubah sebelumnya, sangat mudah untuk memasukkan bug seperti ini. Jika kode membuat kesalahan saat membaca ukuran atau posisi objek pada waktu atau tahap yang salah (misalnya, kita tidak "menghapus" ukuran atau posisi sebelumnya), kita akan segera menambahkan bug histeresis yang halus. Bug ini biasanya tidak muncul dalam pengujian karena sebagian besar pengujian berfokus pada satu tata letak dan render. Yang lebih mengkhawatirkan, kami tahu bahwa beberapa histerisis ini diperlukan agar beberapa mode tata letak berfungsi dengan benar. Kami mengalami bug saat melakukan pengoptimalan untuk menghapus penerusan tata letak, tetapi menyebabkan "bug" karena mode tata letak memerlukan dua penerusan untuk mendapatkan output yang benar.

Hierarki 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 mengurangi jenis bug ini secara luas dari sistem tata letak.

Pembatalan validasi dan performa yang berlebihan

Hal ini merupakan kebalikan langsung dari class bug under-invalidation. Sering kali saat memperbaiki bug pembatalan validasi, kami akan memicu penurunan performa.

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

Munculnya tata letak dua tahap dan penurunan performa

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

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

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

Dua kumpulan kotak, yang pertama menunjukkan ukuran intrinsik kotak dalam penerusan pengukuran, yang kedua pada tata letak semua tinggi yang sama.

Tata letak dua langkah 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 kompleks. Jika Anda tidak meng-cache hasil fase pengukuran, hierarki tata letak akan beralih antara status pengukuran dan status tata letak akhir.

Tata letak satu, dua, dan tiga tahap yang dijelaskan dalam teks.
Pada gambar di atas, kita memiliki tiga elemen <div>. Tata letak satu langkah sederhana (seperti tata letak blok) akan mengunjungi tiga node tata letak (kompleksitas O(n)). Namun, untuk tata letak dua langkah (seperti flex atau petak), hal ini berpotensi menghasilkan kompleksitas kunjungan O(2n) untuk contoh ini.
Grafik yang menunjukkan peningkatan eksponensial dalam waktu tata letak.
Gambar dan demo ini menunjukkan tata letak eksponensial dengan tata letak Petak. Masalah ini diperbaiki di Chrome 93 sebagai akibat dari pemindahan Petak 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 kami berkembang sangat pesat dengan Flex), tetapi kami terus berjuang dengan bug pembatalan validasi yang berlebihan.

LayoutNG memungkinkan kita membuat struktur data eksplisit untuk input dan output tata letak, dan selain itu, kita telah membuat cache pengukuran dan penerusan tata letak. Hal ini mengembalikan kompleksitas ke O(n), sehingga menghasilkan performa linear yang dapat diprediksi untuk developer web. Jika ada kasus saat tata letak melakukan tata letak tiga kali, kita juga akan meng-cache kartu tersebut. Hal ini dapat membuka peluang untuk memperkenalkan mode tata letak yang lebih canggih dengan aman pada masa mendatang. Ini adalah contoh bagaimana RenderingNG secara fundamental membuka ekstensi secara menyeluruh. Dalam beberapa kasus, tata letak Petak dapat memerlukan tata letak tiga tahap, tetapi sangat jarang terjadi pada saat ini.

Kami mendapati bahwa ketika developer mengalami masalah performa, terutama 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 yang mengubah satu properti CSS) menghasilkan tata letak 50-100 md, mungkin ini adalah bug tata letak eksponensial.

Ringkasan

Tata letak adalah area yang sangat kompleks, dan kita tidak membahas semua detail menarik seperti pengoptimalan tata letak inline (benar-benar cara kerja seluruh sub-sistem inline dan teks), dan bahkan konsep yang dibahas di sini hanya sekilas, 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 tahu masih ada banyak pekerjaan yang harus dilakukan. Kami mengetahui jenis masalah (baik performa maupun ketepatan) yang sedang kami pecahkan, dan sangat antusias dengan fitur tata letak baru yang akan hadir di CSS. Kami yakin arsitektur LayoutNG membuat penyelesaian masalah ini aman dan mudah ditangani.

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