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 özellikler geliştiren bir kullanıcı arabirimi mühendisiydim (önceki Google "arayüz mühendisi" olarak görev yapıyordu). 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. Birçok "iyileşen ön uç mühendisinin" benden önce "tarayıcı mühendisi" olmasına sevindim.
Ö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 sistemindeki bu sorunları sistematik olarak düzeltmemize yardımcı olmam için bir fırsattı ve birçok mühendisin yıllar içinde yaptığı çalışmaları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 9.000 metrelik 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 öğelerin 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 önce bir adım geri çekilip düzenin giriş ve çıkışlarını düşünelim.
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. Düzen ağacına hâlâ sahibiz ancak bunu öncelikle düzenin giriş ve çıkışlarını tutmak için kullanırız. Çı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şturmak 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 çok hassas bir sistemin işaretidir. Ö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 içinde esnek düzen ile ilgili olarak yaklaşık 10 hatalık bir 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ı penceresini yeniden boyutlandırmanın veya bir CSS mülkünü sihirli bir şekilde açmanın bu hatayı ortadan kaldırdığı gizemli bir hatayla karşılaştıysanız, hâlâ geçersiz olma sorunuyla karşılaşmışsınız demektir. Değişebilir ağacın bir bölümü etkin olarak temiz kabul edildi, ancak üst kısıtlamalardaki bazı değişiklikler nedeniyle doğru çıktıyı yansıtmadı.
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. Önceden 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 sorunun 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 doğru şekilde 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 farklılaştırdığımız 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ı oluşturmak inanılmaz derecede kolaydı. Kod, yanlış zamanda veya aşamadaki bir nesnenin boyutunu veya konumunu okuma hatasına neden olursa (örneğin, önceki boyutu veya konumu "temizlemediğimiz" için) hemen bir hissterez 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 ederek 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 yükselişi ve performans uçurumları
Esnek ve ızgara düzeni, web'deki düzenlerin ifade gücünde bir değişikliği 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 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 öğ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ılandır.
Kullanıcılar tarafından çok iç içe yerleşmediği için bu iki geçişli düzenler, başlangıçta performans açısından kabul edilebilir düzeydeydi. 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 çarpıtır.
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, düzenin hem girişi hem çıkışı için açık veri yapıları oluşturmamıza olanak tanır. Ayrıca bunun üzerine, ölçüm ve düzen geçişlerinin önbelleklerini oluştururuz. 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üzen son derece karmaşık bir alandır ve satır içi düzen optimizasyonları (satır içi ve metin alt sisteminin tamamının işleyiş şekli) gibi her türlü ilginç ayrıntıyı ele almadık. Burada bahsedilen kavramlar bile yalnızca yüzeysel bir incelemeyi kapsar ve birçok ayrıntıyı göz ardı eder. 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, ö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!).