Ben Ian Kilpatrick. Koji Ishii ile birlikte Blink düzeni ekibinin mühendislik yöneticisiyim. Blink ekibinde çalışmaya başlamadan önce Google Dokümanlar, Drive ve Gmail'de özellik geliştiren bir ön uç mühendisiydim (Google'da "ön uç mühendisi" rolü yoktu). Bu rolde yaklaşık beş yıl çalıştıktan sonra Blink ekibine geçmek için büyük bir risk aldım. C++'yı iş başında etkili bir şekilde öğrendim ve son derece karmaşık Blink kod tabanını geliştirmeye çalıştım. Bugün bile bunun yalnızca küçük bir kısmını anlıyorum. Bu süreçte bana tanınan süre için teşekkür ederim. "İyileşen ön uç mühendislerinin" birçoğunun benden önce "tarayıcı mühendisi" olmaya geçiş yapması beni rahatlattı.
Önceki deneyimlerim, Blink ekibindeyken bana kişisel olarak rehberlik etti. Ön uç mühendisi olarak sürekli olarak tarayıcı tutarsızlıkları, performans sorunları, oluşturma hataları ve eksik özelliklerle karşılaşıyordum. LayoutNG, Blink'in düzen sisteminde bu sorunları sistematik olarak düzeltmeme yardımcı olan bir fırsattı ve birçok mühendisin yıllar boyunca gösterdiği çabaların toplamını temsil ediyor.
Bu yayında, bu gibi büyük bir mimari değişikliğinin çeşitli hata ve performans sorunlarını nasıl azaltabileceğini açıklayacağım.
Düzen motoru mimarilerinin 10.000 metre yükseklikten görünümü
Daha önce Blink'in düzen ağacı, "değişken ağaç" olarak adlandıracağım şekildeydi.
Sayfa düzeni ağacındaki her nesne, giriş bilgilerini (ör. üst öğe tarafından uygulanan kullanılabilir boyut, tüm yüzen noktaların konumu) ve çıktı bilgilerini (ör. nesnenin nihai genişliği ve yüksekliği ya da x ve y konumu) içeriyordu.
Bu nesneler, oluşturma işlemleri arasında tutuldu. Stil değişikliği olduğunda, söz konusu nesneyi ve ağaçtaki tüm üst öğelerini kirli olarak işaretledik. Oluşturma ardışık düzeninin düzen aşaması çalıştırıldığında, ağacı temizler, kirli nesneleri gezer ve ardından bunları temiz bir duruma getirmek için düzeni çalıştırırdık.
Bu mimarinin birçok sorun sınıfına yol açtığını tespit ettik. Bu sınıfları aşağıda açıklayacağız. Ancak öncelikle bir adım geri çekilip düzenin giriş ve çıkışlarını inceleyelim.
Bu ağaçtaki bir düğümde düzen çalıştırmak, kavramsal olarak "stil artı DOM"u ve üst düzen sisteminden (tablo, blok veya esnek) tüm üst öğe kısıtlamalarını alır, düzen kısıtlama algoritmasını çalıştırır ve bir sonuç verir.
Yeni mimarimiz bu kavramsal modeli resmileştirir. Sayfa düzeni ağacını kullanmaya devam ediyoruz ancak bunu öncelikle sayfa düzeninin giriş ve çıkışlarını tutmak için kullanıyoruz. Çıktı için parça ağacı adlı tamamen yeni ve değişmez bir nesne oluştururuz.
Değiştirilemez parça ağacını daha önce ele almıştık. Bu ağacın, artımlı düzenler için önceki ağacın büyük bölümlerini yeniden kullanacak şekilde nasıl tasarlandığını açıklamıştık.
Ayrıca, bu parçayı oluşturan üst kısıtlamalar nesnesini de depolarız. Bu değeri, aşağıda daha ayrıntılı olarak ele alacağımız önbelleğe alma anahtarı olarak kullanırız.
Satır içi (metin) düzen algoritması da yeni değişmez mimariye uyacak şekilde yeniden yazıldı. Satır içi düzen için yalnızca değişmez düz liste gösterimi oluşturmakla kalmaz, daha hızlı yeniden düzen oluşturma için paragraf düzeyinde önbelleğe alma, öğeler ve kelimeler arasında yazı tipi özelliklerini uygulamak için paragraf başına şekil, ICU kullanan yeni bir Unicode iki yönlü algoritma, birçok doğruluk düzeltmesi ve daha fazlasını sunar.
Sayfa düzeni hatası türleri
Genel olarak sayfa düzeni hataları, her biri farklı temel nedenleri olan dört farklı kategoriye ayrılır.
Doğruluk
Oluşturma sistemindeki hataları düşündüğümüzde genellikle doğruluğu düşünürüz. Örneğin: "A tarayıcısı X davranışına, B tarayıcısı ise Y davranışına sahip" veya "A ve B tarayıcıları bozuk". Daha önce zamanımızın çoğunu bu konuya ayırıyorduk ve bu süreçte sürekli sistemle mücadele ediyorduk. Sık karşılaşılan bir hata türü, bir hata için çok hedefli bir düzeltme uygulamak ancak haftalar sonra sistemin başka bir (ilgili görünmeyen) bölümünde gerileme yaşandığını fark etmekti.
Önceki yayınlarda açıklandığı gibi, bu durum sistemin çok kırılgan olduğunun bir göstergesidir. Özellikle düzen için sınıflar arasında net bir sözleşmemiz yoktu. Bu da tarayıcı mühendislerinin gerekmediği halde duruma bağlı kalmasına veya sistemin başka bir kısmındaki bazı değerleri yanlış yorumlamasına neden oluyordu.
Örneğin, bir noktada bir yıldan uzun bir süre boyunca flex düzeniyle ilgili yaklaşık 10 hata zincirimiz vardı. Her düzeltme, sistemin bir kısmında doğruluk veya performans sorununa neden olarak başka bir hataya yol açıyordu.
LayoutNG, artık 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 uygulayabileceğimizi fark ettik. Ayrıca, birden fazla tarafın ortak bir web testi paketine katkıda bulunmasına olanak tanıyan mükemmel Web Platform Testleri (WPT) projesinden de büyük fayda sağlıyoruz.
Bugün, kararlı kanalımızda gerçek bir gerileme yayınlarsak bunun genellikle WPT deposunda ilişkili testleri 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 ekleyerek hiçbir tarayıcının aynı hatayı tekrar yapmamasını sağlıyoruz.
Geçersiz kılma
Tarayıcı penceresinin boyutunu değiştirmenin veya bir CSS özelliğini etkinleştirmenin sihirli bir şekilde hatayı ortadan kaldırdığı gizemli bir hatayla karşılaştıysanız geçersiz kılma işleminin yetersiz olmasıyla ilgili bir sorunla karşılaşmışsınız demektir. Değiştirilebilir ağacın bir kısmı temiz olarak kabul edildi ancak üst öğe kısıtlamalarında yapılan bir değişiklik nedeniyle doğru çıkışı temsil etmedi.
Bu durum, aşağıda açıklanan iki geçişli (nihai düzen durumunu belirlemek için düzen ağacında iki kez gezinme) düzen modlarında çok yaygındır. Daha önce kodumuz şu şekilde görünüyordu:
if (/* some very complicated statement */) {
child->ForceLayout();
}
Bu tür bir hatanın düzeltmesi genellikle şu şekilde olur:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
Bu tür bir sorun için yapılan düzeltme genellikle ciddi bir performans gerilemesine neden olur (aşağıdaki aşırı geçersiz kılma bölümüne bakın) ve düzeltmenin doğru yapılması çok hassas bir işlemdir.
Şu anda (yukarıda açıklandığı gibi) üst düzenden alt öğeye gelen tüm girişleri açıklayan, değiştirilemeyen bir üst öğe kısıtlamaları nesnesi kullanıyoruz. Bu değeri, oluşturulan değiştirilemez parçayla birlikte depolarız. Bu nedenle, çocuğun başka bir düzen geçişi gerçekleştirmesi gerekip gerekmediğini belirlemek için bu iki girişi diff merkezi bir yerimiz vardır. Bu karşılaştırma mantığı karmaşıktır ancak iyi bir şekilde sınırlandırılmıştır. Geçersiz kılma işleminin yetersiz olduğu bu tür sorunların hata ayıklama işlemi genellikle iki girişin manuel olarak incelenmesine ve girişte başka bir düzen geçişi gerektirecek şekilde nelerin değiştiğine karar verilmesine neden olur.
Bu karşılaştırma kodunda yapılan düzeltmeler genellikle basittir ve bu bağımsız nesneleri oluşturmanın basitliği nedeniyle kolayca birim test edilebilir.
Yukarıdaki örnek için fark bulma 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ı, geçersiz kılma işleminin eksik yapılmasına benzer. Özetle, önceki sistemde düzenin tekil olmasını sağlamak son derece zordu. Yani düzenin aynı girişlerle yeniden çalıştırılması aynı çıktıyla sonuçlanıyordu.
Aşağıdaki örnekte, bir CSS özelliğini iki değer arasında ileri geri değiştiriyoruz. Ancak bu, "sonsuz şekilde büyüyen" bir dikdörtgenle sonuçlanır.
Önceki değiştirilebilir ağacımızda bu tür hataların ortaya çıkması inanılmaz derecede kolaydı. Kod, bir nesnenin boyutunu veya konumunu yanlış zamanda ya da aşamada okuma hatası yaptıysa (örneğin, önceki boyutu veya konumu "temizlemedik") hemen ince bir histerezis hatası ekleriz. Testlerin çoğu tek bir düzene ve oluşturmaya odaklandığından bu hatalar genellikle testlerde görünmez. Daha da endişe verici olan, bazı düzen modlarının düzgün çalışması için bu histerezinin bir kısmının gerekli olduğuydu. Bir düzen geçişini kaldırmak için optimizasyon yaptığımız ancak düzen modu doğru çıkışı almak için iki geçiş gerektirdiği için bir "hata" eklediğimiz hatalar vardı.
LayoutNG'de açık giriş ve çıkış veri yapılarına sahip olduğumuz ve önceki duruma erişime izin verilmediğinden, bu tür hataları düzen sisteminden büyük ölçüde azalttık.
Aşırı geçersiz kılma ve performans
Bu, geçersiz kılma kapsamının altındaki hata sınıfının tam tersidir. Çoğu zaman, geçersiz kılma işleminin yetersiz olduğu bir hatayı düzeltirken performansta ani düşüşler yaşıyorduk.
Genellikle performans yerine doğruluğu tercih eden zor seçimler yapmak zorunda kaldık. 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 düzenlerin yükselişi ve performans uçurumları
Esnek ve ızgara düzeni, web'deki düzenlerin ifade gücünde bir değişimi temsil ediyordu. Ancak bu algoritmalar, kendilerinden önce gelen blok düzeni algoritmasından temel olarak farklıydı.
Blok düzeni (neredeyse her durumda), motorun tüm alt öğelerinde düzeni tam olarak bir kez gerçekleştirmesini gerektirir. Bu, performans açısından mükemmel olsa da web geliştiricilerinin istediği kadar etkileyici olmaz.
Örneğin, genellikle tüm alt öğelerin boyutunun en büyük öğenin boyutuna genişletilmesini istersiniz. Bunu desteklemek için üst öğe düzeni (flex veya ızgara), her alt öğenin ne kadar büyük olduğunu belirlemek için 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ılan ayardır.
Kullanıcılar genellikle bunları derinlemesine iç içe yerleştirmediğinden, bu iki geçişli düzenler başlangıçta performans açısından kabul edilebilirdi. 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 nihai düzen durumu arasında gidip gelir.
Daha önce bu tür performans düşüşleriyle mücadele etmek için esnek ve ızgara düzenine çok özel önbellekler eklemeye çalışıyorduk. Bu yöntem işe yaradı (ve Flex ile çok yol kat ettik) ancak sürekli olarak geçersiz kılma hataları ile mücadele ediyorduk.
LayoutNG, hem düzenin girişi hem de çıkışı için net veri yapıları oluşturmamıza olanak tanır. Bunun üzerine, ölçüm ve düzen geçişlerinin önbelleğini oluşturduk. Bu, karmaşıklığı O(n) değerine geri getirir ve web geliştiricileri için tahmin edilebilir şekilde doğrusal bir performans sağlar. Bir düzenin üç geçişli düzen yaptığı bir durum varsa bu geçişi de önbelleğe alırız. Bu, gelecekte daha gelişmiş düzen modlarını güvenli bir şekilde kullanıma sunma fırsatları sunabilir. RenderingNG'nin temel olarak her alanda genişletilebilirliği nasıl sağladığını gösteren bir örnektir. Bazı durumlarda ızgara düzeni için üç geçişli düzenler gerekebilir ancak bu durum şu anda çok nadirdir.
Geliştiricilerin özellikle düzenle ilgili performans sorunlarına rastlamasının, genellikle ardışık düzen aşamasının ham işleme hızından ziyade üstel düzen süresi hatasından kaynaklandığını tespit ettik. Küçük bir artımlı değişiklik (tek bir CSS mülkünü değiştiren bir öğe) 50-100 ms'lik bir düzenle sonuçlanıyorsa bu büyük olasılıkla üstel bir düzen hatası olabilir.
Özet olarak
Düzenleme çok karmaşık bir konudur. Satır içi düzen optimizasyonları (satır içi ve metin alt sisteminin işleyiş şekli) gibi ilginç ayrıntıların hepsini ele almadık. Burada bahsedilen kavramlar bile konuyu sadece yüzeysel olarak ele aldı ve birçok ayrıntıyı atladı. Ancak bir sistemin mimarisini sistematik olarak iyileştirmenin uzun vadede ne kadar büyük kazançlar sağlayabileceğini gösterdiğimizi umuyoruz.
Bununla birlikte, önümüzde hâlâ çok iş olduğunun farkındayız. Çözümü üzerinde çalıştığımız sorun sınıflarının (hem performans hem de doğruluk) farkındayız ve CSS'ye eklenen yeni düzen özellikleri bizi heyecanlandırıyor. LayoutNG'nin mimarisinin bu sorunları güvenli ve kolay bir şekilde çözmeye yardımcı olduğuna inanıyoruz.
Una Kravets'in bir resmi (hangisi olduğunu biliyorsunuz!).