renderNG ayrıntılı incelemesi: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Ben Ian Kilpatrick. Koji Ishii ile birlikte Blink düzen ekibinde lider mühendisim. Blink ekibinde çalışmaya başlamadan önce Kullanıcı arabirimi mühendisiydim (Google "arayüz mühendisi" rolüne sahip olmadan önce), Google Dokümanlar, Drive ve Gmail'de bina özellikleri oluşturabilirsiniz. Bu pozisyonda yaklaşık beş yıl geçirdikten sonra büyük bir kumar oynayarak Blink ekibine geçtim. C++'yı daha etkili bir şekilde öğrendiğinize göre, ve çok karmaşık Blink kod tabanında gelişmeye çalışıyor. Bugün bile bunun nispeten küçük bir kısmını anlıyorum. Bu dönemde bana zaman ayırdığınız için teşekkür ederim. Bir sürü "ön uç mühendisini kurtarmak" beni rahatlattı "tarayıcı mühendisi" olarak geçiş yaptılar. göz atmayı unutmayın.

Blink ekibinde çalışırken önceki deneyimlerim bana şahsen yol gösterdi. Ön uç mühendisi olarak sürekli tarayıcı tutarsızlıklarıyla karşılaşıyorum, performans sorunları, oluşturma hataları ve eksik özellikler. LayoutNG, Blink'in düzen sistemindeki bu sorunları sistematik olarak düzeltmeme yardımcı olmam için bir fırsattı. ve birçok mühendisin toplamını ve çaba sarf etti.

Bu gönderide, bunun gibi büyük bir mimari değişikliğinin çeşitli hata türlerini ve performans sorunlarını nasıl azaltıp azaltabileceğini açıklayacağım.

Düzen motoru mimarilerinin 9.000 metrelik görünümü

Daha önce "değişken ağaç" olarak adlandıracağım Blink'in düzen ağacıydı.

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

Düzen ağacındaki her nesne giriş bilgileri içerir. örneğin bir ebeveyn tarafından uygulanan mevcut boyut, kayan öğelerin konumunu ve çıkış bilgilerini, Örneğin, nesnenin son genişliği ve yüksekliği veya x ve y konumu.

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

Bu mimarinin birçok farklı türde soruna yol açtığını tespit ettik. (bunları aşağıda açıklayacağız) Ama öncelikle bir adım geri gidip düzenin giriş ve çıkışlarının neler olduğunu düşünelim.

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

Daha önce açıklanan kavramsal model.

Yeni mimarimiz, bu kavramsal modeli resmileştiriyor. Düzen ağacımız hâlâ mevcut ancak onu öncelikle düzenin giriş ve çıkışlarını korumak için kullanıyoruz. Çıktı için immutable adı verilen tamamen yeni ve immutable bir nesne oluştururuz.

Parça ağacı.

Ben sabitlemek gerekir önceki ağacın büyük bir bölümünü artımlı düzenler için yeniden kullanmak üzere nasıl tasarlandığını açıklıyor.

Ek olarak, bu parçayı oluşturan üst kısıtlamalar nesnesini saklarız. Bunu bir önbellek anahtarı olarak kullanırız. Bunu aşağıda daha ayrıntılı olarak açıklayacağız.

Satır içi (metin) düzen algoritması da yeni sabit mimariye uyacak şekilde yeniden yazılır. Bu yalnızca değişmez düz liste temsili satır içi düzen için uygundur, ancak aynı zamanda daha hızlı geçiş için paragraf düzeyinde önbelleğe alma özelliğine sahiptir. Öğelere ve kelimelere yazı tipi özellikleri uygulamak için paragraf başına şekil, ICU, çok sayıda doğruluk düzeltmesi ve daha fazlasını kullanan çift yönlü yeni bir Unicode algoritması.

Düzen hatası türleri

Genel anlamda düzen hataları dört farklı kategoriye ayrılır: her birinin temel nedenleri vardır.

Doğruluk

Oluşturma sistemindeki hataları düşündüğümüzde, genellikle doğruluğu, örneğin: "Tarayıcı A'da X davranışı varken Tarayıcı B'de Y davranışı vardır", veya "Tarayıcı A ve B bozuk". Eskiden bu konu üzerinde çok zaman harcıyorduk. ve bu süreçte sistemle sürekli kavga ediyorduk. Yaygın hata modlarından biri, bir hata için hedefi belirli bir düzeltme yapmaktı. ancak haftalar sonra sistemin başka bir (görünürde alakasız) bölümünde bir gerilemeye neden olduğumuzu fark ettik.

Önceki yayınlarda açıklandığı gibi, bu çok hassas bir sistemin göstergesidir. Özel olarak düzen açısından, hiçbir sınıf arasında net bir sözleşmemiz yoktu, durumlarına güvenmemeleri gerekir. Bu da, tarayıcı mühendisliğinin veya sistemin başka bir parçasındaki bir değeri yanlış yorumlayabilir.

Örneğin bir noktada, bir yıldan uzun bir süre içinde yaklaşık 10 hatadan oluşan bir zincirimiz vardı. esnek düzeniyle ilgili. Her düzeltme sistemde bir doğruluk veya performans sorununa neden oldu, bu da bir başka hataya neden oluyor.

Artık LayoutNG, düzen sistemindeki tüm bileşenler arasındaki sözleşmeyi açıkça tanımladığından, değişiklikleri daha güvenle uygulayabileceğimizi gördük. Mükemmel Web Platformu Testleri (WPT) projesinden de büyük ölçüde faydalanıyoruz. Ortak bir web test paketine birden fazla tarafın katkıda bulunmasını sağlar.

Bugün kararlı kanalımızda gerçek bir regresyon yayınlarsak genellikle WPT deposunda ilişkili testi yoktur. ve bileşen sözleşmelerinin yanlış anlaşılmasından kaynaklanmaz. Ayrıca, hata düzeltme politikamız kapsamında her zaman yeni bir WPT testi ekliyoruz. Böylece hiçbir tarayıcının aynı hatayı tekrarlamamasını sağlayabilirsiniz.

Geçersiz kılma işlemi

Tarayıcı penceresini yeniden boyutlandırmanın veya bir CSS mülkünün sihirli bir şekilde değiştirilmesinin bu hatayı ortadan kaldırdığı gizemli bir hatayla karşılaştıysanız, geçersiz kılmayla ilgili bir sorunla karşılaştınız. Değişebilir ağacın bir kısmı etkili bir şekilde temiz bulundu, ancak üst kısıtlamalardaki bazı değişiklikler nedeniyle doğru çıkışı temsil etmedi.

Bu, iki geçişli kartta çok yaygındır. (nihai düzen durumunu belirlemek için düzen ağacını iki kez yürüme) düzen modlarını kullanabilirsiniz. Önceden kodumuz şu şekilde görünüyordu:

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

Bu tür hataların çözümü genellikle şu şekildedir:

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

Bu tür sorunların çözümü genellikle ciddi bir performans regresyonuna neden olur. (aşağıda aşırı geçersiz kılma konusuna bakın) ve hatamızı düzeltmek için çok hassastı.

Bugün (yukarıda açıklandığı gibi), üst düzenden alt öğeye tüm girişleri açıklayan bir sabit üst kısıtlamalar nesnemiz var. Bunu, elde edilen sabit parçayla birlikte saklarız. İşte bu sebeple Çocuğun düzen için başka bir geçiş yapması gerekip gerekmediğini belirlemek amacıyla bu iki girişi farklılaştırdığımız merkezi bir platform bulunur. Bu farklılaştırma mantığı karmaşık olmakla birlikte oldukça bağımsızdır. Bu tür geçersiz kılma sorunları sınıfındaki hataların ayıklanması genellikle iki girişin de manuel olarak denetlenmesiyle sonuçlanır. ve girişte neyin yeni bir düzen geçişi gerekecek şekilde değiştiğine karar verme.

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

Sabit genişlik ve yüzde genişliğinde bir resmin karşılaştırması.
Sabit genişlik/yükseklik öğesi, kendisine verilen kullanılabilir boyutun artmasını önemsemez ancak yüzdeye dayalı bir genişlik/yükseklik ekler. available-size, available-size nesnesinde temsil edilir ve fark algoritmasının bir parçası olarak bu optimizasyonu gerçekleştirir.

Yukarıdaki örnek için fark kodu şöyledir:

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

Histerez

Bu hata sınıfı, gereğinden az geçersiz kılmaya benzer. Esasen, önceki sistemde düzenin doğaçlama olduğundan, diğer bir deyişle, aynı girişlerle yeniden çalıştırıldığında aynı çıkışla sonuçlandı.

Aşağıdaki örnekte, bir CSS mülkünü iki değer arasında değiştiriyoruz. Ancak bu, "sürekli artan" kullanabilirsiniz.

Videoda ve demoda, Chrome 92 ve önceki sürümlerdeki bir histerez hatası gösteriliyor. Bu sorun Chrome 93 sürümünde düzeltilmiştir.

Önceki değişken ağacımızda bunun gibi hataları kullanıma sunmak çok kolaydı. Kod, yanlış zamanda veya aşamada bir nesnenin boyutunu veya konumunu okuma hatasına neden olduysa (örneğin, önceki boyutu veya konumu "temizlemediğimiz" için) hemen küçük bir hissterez hatası eklerdik. Testlerin büyük kısmı tek bir düzene ve oluşturmaya odaklandığından bu hatalar genellikle testlerde görünmez. Daha da kötüsü, bazı düzen modlarının doğru çalışması için bu histerenin bir kısmının gerekli olduğunu biliyorduk. Düzen geçişini kaldırmak için optimizasyon yapacak hatalarımız vardı. ama kullanıcılara bir “hata” çünkü düzen modunun doğru çıkışı almak için iki geçiş geçişi gerekiyor.

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

LayoutNG ile, açık giriş ve çıkış veri yapılarına sahip olduğumuzdan ve önceki duruma erişmeye izin verilmiyor. Bu nedenle, düzen sisteminden bu hata sınıfını büyük ölçüde azalttık.

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

Bu, geçersiz kılınmayan hata sınıfının doğrudan tersidir. Geçersiz kılınmayan bir hatayı düzeltirken genellikle performans uçurumunu tetikleriz.

Sık sık, performanstan çok doğruluğu ön plana çıkarmak için zor seçimler yapmak zorunda kaldık. 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 düzenlerin ve performans uçurumlarının yükselişi

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

Blok düzeni (neredeyse her durumda), motorun düzeni tüm alt öğeleri üzerinde tam olarak bir kez gerçekleştirmesini gerektirir. Bu, performans açısından harikadır ancak sonuçta web geliştiricilerin istediği kadar iyi yansıtılamaz.

Örneğin, genellikle tüm alt öğelerin boyutunun en büyük olacak şekilde genişletilmesini istersiniz. Bunu desteklemek için üst düzen (esnek veya ızgara) her bir çocuğun büyüklüğünü belirlemek için ölçüm geçişi gerçekleştirecek. tüm alt öğeleri bu boyuta genişletmek için bir düzen geçişi yapın. Bu davranış, hem esnek hem de ızgara düzeni için varsayılandır.

İki kutu; ilki, ölçüm aktarımındaki kutuların içsel boyutunu gösterir, ikincisi ise düzenin tamamı eşit yüksekliktedir.

Bu iki geçişli düzenler başlangıçta performans açısından kabul edilebilirdi. pek iç içe kullanmıyordu. 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ı, measure durumu ile son düzen durumu arasında takılır.

Altyazıda açıklanan bir, iki ve üç geçişli düzenler.
Yukarıdaki resimde üç <div> öğemiz var. Tek geçişli basit bir düzen (blok düzeni gibi) üç düzen düğümünü (karmaşıklık O(n)) ziyaret eder. Ancak iki geçişli düzen (esnek veya ızgara gibi) için bu durum potansiyel olarak söz konusu örnekte O(2n) ziyaretlerinin karmaşıklığına yol açabilir.
'nı inceleyin.
Düzen süresindeki üstel artışı gösteren grafik.
Bu resimde ve demo bölümünde Izgara düzenine sahip üstel bir düzen gösterilmektedir. Bu sorun, Grid'in yeni mimariye taşınması sonucunda Chrome 93'te düzeltilmiştir
'nı inceleyin.

Daha önce, bu tür performans uçurumlarıyla mücadele etmek amacıyla esnek ve ızgara düzenine çok özel önbellekler eklemeye çalışıyorduk. Bu işe yaradı (ve Flex'le çok büyük ilerleme kaydettik), .

LayoutNG, düzenin hem girişi hem çıkışı için açık veri yapıları oluşturmamıza olanak tanır. Bunların da ötesinde, ölçüm ve düzen kartlarının önbelleklerini oluşturduk. Bu da karmaşıklığı tekrar O(n) haline getirir. Bu da web geliştiricileri için öngörülebilir doğrusal performans sağlar. Bir düzenin üç geçişli düzen yaptığı bir durum söz konusu olduğunda, o geçişi de önbelleğe alırız. Bu, gelecekte daha gelişmiş düzen modlarını güvenli bir şekilde tanıtma fırsatlarının kapısını açabilir. RenderingNG'nin temel olarak nasıl kullanıldığına dair bir örnek. genişletilebilirliği mümkün kılar. Izgara düzeni bazı durumlarda üç geçişli düzenler gerektirebilir, ancak şu an için çok nadirdir.

Geliştiriciler özellikle düzende performans sorunlarıyla karşılaştıklarında genellikle ardışık düzenin düzen aşamasının ham işleme hızından ziyade üstel düzen zamanı hatasından kaynaklanır. Küçük bir artımlı değişiklik (tek bir css özelliğini değiştiren bir öğe) 50-100 ms düzeniyle sonuçlanırsa bu muhtemelen üstel düzen hatasıdır.

Özet olarak

Düzen, çok karmaşık bir alandır. satır içi düzen optimizasyonları gibi birçok ilginç ayrıntıyı ele (satır içi ve metin alt sisteminin tamamının nasıl çalıştığı) Burada bahsettiğimiz kavramlar bile yalnızca yolun başında ve birçok ayrıntıya ayrılmıştı. Ancak, bir sistem mimarisini sistematik olarak iyileştirmenin uzun vadede nasıl çok büyük kazanımlar sağlayabileceğini gösterdiğimizi umuyoruz.

Bununla birlikte, hâlâ yapacağımız çok iş olduğunu biliyoruz. Çözmeye çalıştığımız sorun sınıflarının (hem performans hem doğruluk) farkındayız. ve CSS'de kullanıma sunulacak yeni düzen özellikleri konusunda heyecanlı. LayoutNG'un mimarisinin bu sorunların güvenli ve uygulanabilir olmasını sağladığına inanıyoruz.

Una Kravets'ten bir resim (hangisini biliyorsunuz!).