renderNG ayrıntılı incelemesi: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji İşi
Koji Ishi

Ben Ian Kilpatrick. Koji Ishii ile birlikte Blink düzen ekibinde mühendisim. Blink ekibinde çalışmaya başlamadan önce (Google "arayüz mühendisi" rolünü üstlenmeden önce) ön uç mühendisiydim. Ayrıca Google Dokümanlar, Drive ve Gmail'de özellikler geliştiriyordum. Bu pozisyonda yaklaşık beş yıl çalıştıktan sonra Blink ekibine geçerek Bugün bile bu e-postanın nispeten küçük bir kısmını anlayabiliyorum. Bu dönemde bana zaman ayırdığınız için minnettarım. Çok sayıda "kullanıcı arabirimi mühendisini kurtarma" mühendisinin benden önce "tarayıcı mühendisi" olmaya geçiş yapması beni rahatlattı.

Blink ekibindeyken önceki deneyimlerim bana bizzat yol gösterdi. Kullanıcı arabirimi mühendisi olarak sürekli tarayıcı tutarsızlıkları, performans sorunları, oluşturma hataları ve eksik özelliklerle karşılaşıyorum. LayoutNG benim için, Blink'in düzen sistemindeki bu sorunları sistematik olarak düzeltmeme yardımcı olmam için bir fırsattı ve yıllar içinde birçok mühendisin gösterdiği çabanın tamamını temsil ediyor.

Bu yayında, böyle büyük bir mimari değişikliğinin çeşitli hata türlerini ve performans sorunlarını nasıl azaltabileceğini ve azaltabileceğini açıklayacağım.

Düzen motoru mimarilerinin 30.000 fitlik görünümü

Önceden Blink'in düzen ağacı bunu "değişken ağaç" olarak adlandıracağım.

Ağacı aşağıdaki metinde açıklandığı gibi gösterir.

Düzen ağacındaki her nesne, bir üst öğe tarafından uygulanan kullanılabilir boyut, kayan öğelerin konumu ve çıkış bilgileri (örneğin, nesnenin son genişliği ve yüksekliği veya x ve y konumu) gibi girdi bilgileri içeriyordu.

Bu nesneler oluşturma işlemleri arasında tutuldu. Stilde bir değişiklik olduğunda, nesneyi kirli olarak işaretledik. Aynı şekilde, ağaçtaki tüm üst öğeleri de kirli olarak işaretlendi. Oluşturma ardışık düzeninin düzen aşaması çalıştırıldığında, ağacı temizler, kirli nesneleri temizler ve temiz bir duruma getirmek için bir düzen çalıştırırız.

Bu mimarinin çok sayıda soruna yol açtığını tespit ettik. Bunları aşağıda açıklayacağız. Ama ilk önce bir adım geri çekilip düzenin girdi ve çıktılarının neler olduğuna bakalım.

Bu ağaçtaki bir düğümde düzen çalıştırmak, kavramsal olarak "Stil artı DOM" öğesini ve üst düzen sistemindeki tüm üst kısıtlamaları (ızgara, blok veya esnek) alır, düzen sınırlama algoritmasını çalıştırır ve bir sonuç oluşturur.

Daha önce açıklanan kavramsal model.

Yeni mimarimiz, bu kavramsal modeli biçimlendirir. Düzen ağacını kullanmaya devam ediyoruz, ancak bu ağacı öncelikli olarak düzenin girişlerini ve çıkışlarını tutmak için kullanıyoruz. Çıkış için parça ağacı adı verilen tamamen yeni, değişmez bir nesne oluştururuz.

Parça ağacı.

Daha önce sabit parça ağacını ele almış ve artımlı düzenler için önceki ağacın büyük bölümlerini yeniden kullanmak üzere nasıl tasarlandığını açıklamıştım.

Buna ek olarak, söz konusu parçayı oluşturan üst kısıtlamalar nesnesini de depolarız. Bunu, aşağıda daha ayrıntılı olarak ele alacağımız bir önbellek anahtarı olarak kullanırız.

Satır içi (metin) düzen algoritması da yeni sabit mimariyle eşleşecek şekilde yeniden yazılır. Yalnızca satır içi düzen için değişmez düz liste gösterimi oluşturmakla kalmaz, aynı zamanda daha hızlı geçiş için paragraf düzeyinde önbelleğe alma, öğeler ve kelimelere

Düzen hatası türleri

Düzen hataları, genel olarak, her birinin farklı temel nedenleri olan dört farklı kategoriye ayrılır.

Doğruluk

Oluşturma sistemindeki hatalar dediğimizde genellikle doğruluğu düşünüyoruz. Örneğin: "A Tarayıcısı X, B Tarayıcısı Y davranışına sahip" veya "A ve B Tarayıcılarının ikisi de bozuk". Eskiden zamanımızın çoğunu bu şekilde harcıyorduk. Bu süreçte sistemle sürekli kavga ediyorduk. Sık karşılaşılan bir hata modu, bir hata için çok iyi belirlenmiş bir düzeltme uygulamak, ancak haftalar sonra sistemin başka bir (görünüşte alakasız) bölümünde bir regresyona neden olduğumuzu bulmaktı.

Önceki yayınlarda da açıklandığı gibi bu, çok kırılgan bir sistemin işaretidir. Özellikle düzen açısından, sınıfların arasında net bir sözleşme yoktu. Bu da tarayıcı mühendislerinin yapmamaları gereken duruma bağlı olmalarına veya sistemin başka bir kısmındaki bazı değerleri yanlış yorumlamalarına yol açıyordu.

Örnek verecek olursak, bir yıldan uzun bir süre boyunca, esnek düzenle ilgili yaklaşık 10 hata zincirimiz vardı. Her düzeltme, sistemin bir kısmında doğruluk veya performans sorununa yol açmış, bu da başka bir hataya yol açmıştır.

LayoutNG, düzen sistemindeki tüm bileşenler arasındaki sözleşmeyi net bir şekilde tanımladığından, değişiklikleri çok daha güvenle uygulayabildiğimizi gördük. Birden çok tarafın ortak bir web test paketine katkıda bulunmasına olanak tanıyan mükemmel Web Platformu Testleri (WPT) projesinden de büyük ölçüde yararlanıyoruz.

Şu anda kararlı kanalımızda gerçek bir regresyon yayınlarsak bu regresyonun genellikle WPT deposunda ilişkili testlerin olmadığını ve bileşen sözleşmelerinin yanlış anlaşılmasından kaynaklanmadığını görüyoruz. Ayrıca, hata düzeltme politikamız kapsamında her zaman yeni bir WPT testi ekleriz. Böylece, hiçbir tarayıcının aynı hatayı tekrar yapmamasını sağlamaya yardımcı oluruz.

Geçersiz kılma

Tarayıcı penceresini yeniden boyutlandırmanın veya bir CSS özelliğini sihirli bir şekilde değiştirmenin hatanın ortadan kalkmasını sağlayan gizemli bir hata yaşadıysanız, geçersiz kılmayla ilgili bir sorunla karşılaşmışsınız demektir. Değişebilir ağacın bir bölümü etkin olarak temiz kabul edildi, ancak üst öğe kısıtlamalarındaki bazı değişiklikler nedeniyle doğru çıktıyı yansıtmadı.

Bu, aşağıda açıklanan iki geçişli (nihai düzen durumunu belirlemek için düzen ağacında iki kez yürüme) düzen modlarında çok yaygındır. Daha önce kodumuz şöyle görünürdü:

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

Bu tür hatalar için genellikle şöyle bir düzeltme yapılabilir:

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

Bu tür sorunların düzeltilmesi, genellikle ciddi bir performans gerilemesine neden olur (aşağıdaki aşırı geçersiz kılma bölümüne bakın) ve bu sorunun düzeltilmesi çok hassas bir süreçtir.

Bugün (yukarıda açıklandığı gibi), üst düzenden alt öğeye kadar tüm girişleri açıklayan sabit bir üst kısıtlama nesnemiz var. Bunu, elde edilen sabit parçada saklarız. Bu nedenle, alt öğe için başka bir düzen geçişi yapılması gerekip gerekmediğini belirlemek amacıyla bu iki girişi farklılaştırdığımız merkezi bir yer sunuyoruz. Bu farklı mantık karmaşık, ancak tutarlı bir yaklaşım. Bu kadar geçersiz kılma sorunu sınıfında hata ayıklamak, genellikle iki girişin manuel olarak incelenmesiyle ve girişteki nelerin değiştiğine karar verilmesiyle sonuçlanır. Böylece başka bir düzen geçişi gerekir.

Bu fark kodundaki düzeltmeler, bu bağımsız nesneleri oluşturmanın basitliği sayesinde genellikle basittir ve kolayca birim test edilebilir.

Sabit genişlikli ve yüzde genişlikli bir resmin karşılaştırılması.
Sabit bir genişlik/yükseklik öğesi, kendisine verilen kullanılabilir boyutun artmasını önemsemez ancak yüzdeye dayalı bir genişlik/yükseklik öğesi artar. Kullanılabilir-boyut, Üst Kısıtlamalar nesnesinde temsil edilir ve fark algoritmasının bir parçası olarak bu optimizasyonu gerçekleştirir.

Yukarıdaki örnek için fark kodu:

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

Histerezis

Bu hata sınıfı, gereğinden az geçersiz kılma işlemine benzer. Esas olarak, önceki sistemde düzenin eşi benzeri olmayan, diğer bir deyişle düzeni aynı girişlerle yeniden çalıştırarak aynı sonucun elde edilmesini sağlamak son derece zordu.

Aşağıdaki örnekte sadece bir CSS özelliğini iki değer arasında değiştiriyoruz. Ancak bu, "sonsuza kadar büyüyen" bir dikdörtgene neden olur.

Video ile demo, Chrome 92 ve önceki sürümlerde bir hissterez hatası gösteriyor. Bu sorun Chrome 93'te düzeltilmiştir.

Önceki değişken ağacımızda, bunun gibi hataları getirmek son derece kolaydı. Kod, bir nesnenin yanlış zamanda veya aşamadaki boyutunu veya konumunu okuma hatasına düştüyse (örneğin, önceki boyutu veya konumu "temizlemediğimiz" için) hemen göze çarpmayan bir histerez hatası ekleriz. Testlerin çoğu tek bir düzen ve oluşturmaya odaklandığından, bu hatalar genellikle testlerde ortaya çıkmaz. Daha da önemlisi, bazı düzen modlarının doğru çalışmasını sağlamak için bu histerezin bir kısmının gerekli olduğunu biliyorduk. Düzen geçişini kaldırmak için optimizasyon gerçekleştirdiğimiz hatalarla karşılaştık. Ancak, düzen modunda doğru çıktıyı almak için iki geçiş gerektiği için bir "hata" özelliğini kullanıma sunduk.

Önceki metinde açıklanan sorunları gösteren bir ağaç.
Önceki düzen sonucu bilgilerine bağlı olarak, benzersiz olmayan düzenlerle sonuçlanır

LayoutNG'de, açık giriş ve çıkış veri yapılarımız olduğundan ve önceki duruma erişime izin verilmediğinden, bu tür hataları düzen sisteminden genel olarak azalttık.

Aşırı geçersiz kılma ve performans

Bu durum, geçersiz geçersiz kılma sınıfının doğrudan tersidir. Genellikle gereğinden az geçersiz kılınan bir hatayı düzeltirken performans uçurumunu tetikleriz.

Çoğu zaman, performanstan ziyade doğruluğu tercih eden zor seçimler yapmak zorunda kalıyorduk. Bir sonraki bölümde, bu tür performans sorunlarını nasıl azalttığımızı daha ayrıntılı bir şekilde inceleyeceğiz.

İki geçişli yollar ve performans kayalıkları artışı

Esnek ve ızgara düzeni, web'deki düzenlerin ifade gücünde bir değişimi temsil ediyordu. Ancak bu algoritmalar, temelde kendilerinden önce gelen blok düzeni algoritmasından farklıdır.

Blok düzeni (neredeyse her durumda) motorun, tüm alt öğelerinde yalnızca bir kez düzen gerçekleştirmesini gerektirir. Bu, performans için mükemmeldir ancak web geliştiricilerin istediği kadar etkileyici olmaz.

Örneğin, genellikle tüm alt öğelerin boyutunun en büyük olana genişlemesini istersiniz. Bunu desteklemek amacıyla, üst düzen (esnek veya ızgara) her bir alt öğenin ne kadar büyük olduğunu belirlemek üzere bir ölçüm geçişi, ardından tüm alt öğeleri bu boyuta uzatmak için bir düzen geçişi gerçekleştirir. Bu davranış hem esnek hem de ızgara düzeni için varsayılandır.

İki kutu grubu. İlki ölçüm geçişindeki kutuların gerçek boyutunu, ikincisi ise eşit yükseklikteki kutuları gösterir.

Bu iki geçişli düzenler başlangıçta performans açısından kabul edilebilir düzeydeydi, çünkü kullanıcılar genellikle bunları çok fazla iç içe yerleştirmedi. Ancak daha karmaşık içerikler ortaya çıktıkça önemli performans sorunları görmeye başladık. Ölçüm aşamasının sonucunu önbelleğe almazsanız düzen ağacı, ölçüm durumu ile son düzen durumu arasında gidip gelir.

Bir, iki ve üç geçişli düzenlerin açıklaması altyazıdadır.
Yukarıdaki resimde üç <div> öğemiz var. Basit bir tek geçişli düzen (blok düzeni gibi) üç düzen düğümünü ziyaret eder (karmaşıklık O(n)). Bununla birlikte, iki geçişli bir düzende (esnek veya ızgara gibi) bu, söz konusu örnekte O(2n) ziyaretlerinin karmaşıklığına yol açabilir.
Düzen zamanındaki üstel artışı gösteren grafik.
Bu resimde ve demo'da, ızgara düzeninde üstel düzen gösterilmektedir. Bu sorun, Grid'in yeni mimariye taşınmasıyla Chrome 93'te düzeltilmiştir

Daha önce, bu tür performans uçurumlarıyla mücadele etmek için esnek ve ızgara düzenine çok özel önbellekler eklemeye çalışıyorduk. Bu işe yaradı (ve Flex ile çok yol kat ettik) ancak geçersiz kılma hatalarıyla sürekli olarak mücadele ediyorduk.

LayoutNG, düzenin hem girişi hem çıkışı için açık veri yapıları oluşturmamıza olanak tanır. Buna ek olarak, ölçüm ve düzen geçişleri için önbellekler oluşturduk. Bu da karmaşıklığı tekrar O(n) haline getirerek web geliştiricileri için öngörülebilir şekilde doğrusal performans elde edilmesini sağlıyor. Bir düzenin üç geçişli düzen yaptığı durumlar varsa, o düzeni de önbelleğe alırız. Bu, RenderingNG'nin tüm alanda nasıl temel bir genişletilebilirlik kilidi açtığını gösteren bir örnek üzerinden, daha gelişmiş düzen modlarını güvenli bir şekilde kullanıma sunma fırsatı sağlayabilir. Bazı durumlarda Izgara düzeni için üç geçişli düzenler gerekebilir, ancak bu şu anda çok nadir gerçekleştirilen bir düzendir.

Geliştiriciler özellikle düzenle ilgili performans sorunlarıyla karşılaştıklarında, bunun genellikle ardışık düzen aşamasının ham işleme hızından ziyade üstel düzen zamanı hatasından kaynaklandığını tespit ettik. Küçük bir artımlı değişiklik (tek bir css özelliğini değiştiren bir öğe) 50-100 ms'lik bir düzenle sonuçlanırsa, bu büyük olasılıkla üstel düzen hatasıdır.

Özet

Düzen çok karmaşık bir alandır. Satır içi düzen optimizasyonları (gerçekten de tüm satır içi ve metin alt sisteminin çalışma şekli) gibi ilgi çekici ayrıntılara yer vermedik. Böylece, burada bahsedilen kavramlar bile aslında yüzeyden kazındı ve birçok ayrıntıyı gözden kaçırdılar. Ancak, bir sistemin mimarisini sistematik olarak iyileştirmenin uzun vadede çok büyük kazançlar sağlayabileceğini gösterdiğimizi umuyoruz.

Yine de önümüzde hâlâ çok iş olduğunu biliyoruz. Çözmeye çalıştığımız sorun sınıflarının (hem performans hem de doğruluk) farkındayız ve CSS'de kullanıma sunulacak yeni düzen özellikleri bizi heyecanlandırıyor. LayoutNG'nin mimarisinin bu sorunları güvenli ve kolay uygulanabilir hale getirdiğine inanıyoruz.

Una Kravets'ten bir resim (hangisi olduğunu biliyorsunuz!).