Jestem Ian Kilpatrick i kierownikiem zespołu ds. układów Blink oraz Koji Ishii. Zanim dołączyłam do zespołu Blink, byłam inżynierem front-endu (przed pojawieniem się w Google stanowiska „inżynier front-endu”), który tworzy funkcje w Dokumentach Google, Dysku i Gmailu. Po około 5 latach na tym stanowisku przeszedłem do zespołu Blink, skutecznie uczyłem się C++ w pracy i próbowałem wykorzystać mocno złożoną bazę kodu Blink. Nawet dzisiaj rozumiem tylko niewielką część. Dziękuję za poświęcony czas. Pocieszyło mnie to, że wielu „inżynierów front-endu” zdecydowało się zostać „inżynierem przeglądarek”.
Moje wcześniejsze doświadczenie w zespole Blink pomogło mi osobiście. Jako inżynier front-endowy często natrafiałem na niespójności w przeglądarce, problemy z wydajnością, błędy renderowania i brakujące funkcje. Dzięki LayoutNG udało mi się systematycznie rozwiązywać te problemy w systemie układu Blink. Ta wersja jest wynikiem pracy wielu inżynierów na przestrzeni lat.
W tym poście wyjaśnię, jak duża zmiana w architekturze może ograniczyć i zniwelować różne typy błędów oraz problemy z wydajnością.
Architektura silnika układu na 9000 stóp
Wcześniej drzewo układu w Blink było tym, co nazywam „drzewem zmiennym”.
Każdy obiekt w drzewie układu zawierał informacje wejściowe, takie jak dostępny rozmiar narzucony przez obiekt nadrzędny, położenie dowolnych elementów zmiennoprzestrzennych oraz informacje wyjściowe, np. ostateczną szerokość i wysokość obiektu lub jego położenie w kierunku osi X i Y.
Obiekty te były wyświetlane między renderowaniem. Gdy nastąpiła zmiana stylu, oznaczyliśmy ten obiekt jako zmieniony, a także wszystkie jego nadrzędne w drzewie. Po zakończeniu fazy układu w pipeline renderowania czyściliśmy drzewo, sprawdzaliśmy wszystkie brudne obiekty, a następnie wykonywaliśmy układ, aby je wyczyścić.
Okazało się, że ta architektura spowodowała wiele klas problemów, które opisujemy poniżej. Najpierw jednak cofniemy się i zastanowimy się, jakie są dane wejściowe i wyjściowe układu.
Uruchamianie układu w węźle w tym drzewie koncepcyjnie zakłada „Styl plus DOM” i wszystkie ograniczenia nadrzędne z systemu układu nadrzędnego (siatka, blok lub Flex) uruchamia algorytm ograniczenia układu i zwraca wynik.
Nasza nowa architektura formalizuje ten model koncepcyjny. Nadal mamy drzewo układu, ale używamy go głównie do przechowywania danych wejściowych i wyjściowych układu. W przypadku danych wyjściowych generujemy zupełnie nowy, niezmienny obiekt o nazwie drzewo fragmentów.
W poprzednim artykule omawialiśmy niezmienną strukturę fragmentów i opisywaliśmy, jak została ona zaprojektowana, aby można było ponownie używać dużych fragmentów poprzedniej struktury w przypadku układów przyrostowych.
Dodatkowo przechowujemy obiekt nadrzędny ograniczeń, który wygenerował ten fragment. Używamy go jako klucza pamięci podręcznej, o którym więcej powiemy poniżej.
Algorytm układu wstawionego tekstu został też przepisany, aby pasował do nowej niezmiennej architektury. Nie tylko tworzy stałą reprezentację płaskiej listy dla układu wbudowanego, ale udostępnia też buforowanie na poziomie akapitu, które przyspiesza przekazywanie, kształtowanie akapitu w celu zastosowania funkcji czcionek do elementów i słów, nowy dwukierunkowy algorytm Unicode korzystający z modułu ICU oraz wiele poprawek poprawności.
Typy błędów układu
Błędy układu można ogólnie podzielić na 4 kategorie, z różnymi przyczynami.
Poprawność
Gdy mówimy o błędach w systemie renderowania, mamy na myśli poprawność. Przykładowo: „Przeglądarka A ma zachowanie X, a Przeglądarka B – zachowanie Y” lub „Przeglądarki A i B są uszkodzone”. Wcześniej poświęcaliśmy na to dużo czasu, ponieważ ciągle walczyliśmy z systemem. Typowym błędem było zastosowanie bardzo ukierunkowanej poprawki dotyczącej jednego błędu, ale po kilku tygodniach okazało się, że spowodowaliśmy regresję w innej (pozornie niezwiązanej) części systemu.
Jak pisaliśmy w poprzednich postach, jest to oznaka bardzo niestabilnego systemu. W przypadku układu nie mieliśmy czystego powiązania między klasami, co powodowało, że inżynierowie przeglądarek byli zależni od stanu, na którym nie powinni polegać, lub niewłaściwie interpretowali wartość z innej części systemu.
Na przykład w ciągu ponad roku mieliśmy około 10 błędów związanych z flex layoutem. Każda poprawka powodowała problemy z poprawnością lub wydajnością w części systemu, co prowadziło do kolejnych błędów.
Teraz, gdy LayoutNG wyraźnie określa relacje między wszystkimi komponentami systemu układu, możemy wprowadzać zmiany z większym poczuciem pewności. Korzystamy też z wyników projektu Web Platform Tests (WPT), który umożliwia wielu stronom tworzenie wspólnego zestawu testów internetowych.
Obecnie stwierdziliśmy, że jeśli wprowadzimy prawdziwą regresję na stabilnym kanale, zwykle nie ma ona powiązanych testów w repozytorium WPT i nie jest wynikiem niezrozumienia umów dotyczących komponentów. Ponadto zgodnie z naszą polityką dotyczącą poprawek zawsze dodajemy nowy test WPT, aby żadna przeglądarka nie popełniła tego samego błędu ponownie.
Unieważnienie
Jeśli kiedykolwiek spotkałeś się z tajemniczym błędem, który znikał po zmianie rozmiaru okna przeglądarki lub przełączeniu właściwości CSS, prawdopodobnie był to problem z niepełnym unieważnianiem. Część drzewa z możliwością zmiany została uznana za czystą, ale z powodu pewnych zmian w ograniczeniach nadrzędnych nie odpowiadała ona prawidłowemu wynikowi.
Jest to bardzo częste w przypadku trybów z 2 przejściami (dwukrotne przejście drzewa układu, aby określić ostateczny stan układu) w trybach układu opisanych poniżej. Wcześniej nasz kod wyglądał tak:
if (/* some very complicated statement */) {
child->ForceLayout();
}
W przypadku tego typu błędów zwykle należy:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
Rozwiązanie tego typu problemu zwykle powodowało poważne pogorszenie wydajności (patrz poniżej na temat nadmiernej nieważności) i było bardzo trudne do naprawienia.
Obecnie (jak opisano powyżej) mamy niezmienny obiekt ograniczeń rodzica, który opisuje wszystkie dane wejściowe od układu nadrzędnego do podrzędnego. Przechowujemy go razem z otrzymanym niemodyfikowalnym fragmentem. Mamy więc centralne miejsce, w którym różnicujemy te dane wejściowe, aby określić, czy element podrzędny wymaga wykonania kolejnej karty układu. Ta różnica logiki jest skomplikowana, ale pełna i dokładna. Debugowanie tej klasy problemów z niepełnym unieważnianiem zwykle wymaga ręcznego sprawdzenia obu danych wejściowych i podjęcia decyzji, co się w nich zmieniło, że wymagane jest ponowne wykonanie passu układu.
Poprawki w tym kodzie porównywania są zwykle proste i łatwe do testowania jednostkowego ze względu na prostotę tworzenia tych niezależnych obiektów.
Kod porównywania w przykładzie powyżej:
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
Hysteresis
Ta klasa błędów jest podobna do niepełnego unieważnienia. W poprzednim systemie bardzo trudno było zapewnić idempotentność układu, czyli powtórne uruchomienie układu z tymi samymi danymi wejściowymi dawało ten sam wynik.
W przykładzie poniżej po prostu przełączamy właściwość CSS między 2 wartościami. Jednak powoduje to „nieskończone” powiększanie prostokąta.
W poprzednim zmiennym drzewie wprowadzanie takich błędów było niezwykle proste. Jeśli kod popełnił błąd, odczytując rozmiar lub położenie obiektu w nieprawidłowym czasie lub na etapie (np. ponieważ nie „wyczyściliśmy” poprzedniego rozmiaru lub pozycji), od razu dodaliśmy subtelną usterkę histezy. Te błędy zwykle nie pojawiają się podczas testowania, ponieważ większość testów koncentruje się na jednym układzie i renderowaniu. Co bardziej niepokojące, zdawaliśmy sobie sprawę, że do prawidłowego działania niektórych trybów układu potrzebna jest pewna histereza. Wystąpiły błędy, w których przypadku optymalizacja polegała na usunięciu przejścia układu, ale wprowadzała „błąd”, ponieważ tryb układu wymagał 2 przejść, aby uzyskać prawidłowe dane wyjściowe.
W przypadku LayoutNG, ponieważ mamy wyraźne struktury danych wejściowych i wyjściowych, a dostęp do poprzedniego stanu jest niedozwolony, udało nam się wyeliminować tę klasę błędów z systemu układu.
Nadmierne unieważniania i wydajność
Jest to bezpośrednie przeciwieństwo klasy błędów, które nie pozwalają na ustalenie zasad. Często podczas naprawiania błędu związanego z niepełnym unieważnieniem powodowaliśmy spadek wydajności.
Często musieliśmy podejmować trudne decyzje, wybierając poprawność kosztem wydajności. W następnej sekcji omówimy szczegółowo, jak rozwiązaliśmy te problemy.
Układy z 2 przelotami i spadek wydajności
Układy flex i siatka to zmiana w wyrazistości układów w internecie. Jednak te algorytmy różniły się zasadniczo od wcześniejszego algorytmu układu bloków.
Blok układu (w prawie wszystkich przypadkach) wymaga od silnika wykonania układu wszystkich jego elementów dokładnie raz. To świetne rozwiązanie pod względem wydajności, ale nie jest tak efektywne, jak chcieliby to programiści.
Często na przykład chcesz, aby rozmiar wszystkich elementów potomnych był taki sam jak rozmiar największego z nich. Aby to umożliwić, układ nadrzędny (elastyczny lub siatkowany) wykona pomiar, aby określić rozmiar każdego elementu podrzędnego, a następnie wykona pomiar układu, aby rozciągnąć wszystkie elementy podrzędne do tego rozmiaru. To zachowanie jest domyślne zarówno w przypadku układu elastycznego, jak i układu siatki.
Te układy z 2 przelotami początkowo zapewniały akceptowalną wydajność, ponieważ użytkownicy zwykle nie umieszczali ich głęboko w hierarchii. Jednak wraz z pojawianiem się bardziej złożonych treści zaczęły się pojawiać poważne problemy z wydajnością. Jeśli nie zapiszesz w pamięci podręcznej wyniku fazy pomiaru, drzewo układu będzie się przełączać między stanem pomiaru a ostatecznym stanem układu.
Wcześniej, aby rozwiązać ten problem, próbowaliśmy dodawać do układów elastycznych i siatek bardzo konkretne pamięci podręczne. To zadziałało (i doszliśmy bardzo daleko z Flex), ale ciągle walczyliśmy z błędami związanymi z niepełnym i zbyt dużym unieważnieniem.
LayoutNG pozwala tworzyć wyraźne struktury danych zarówno dla danych wejściowych, jak i wyjściowych układu, a dodatkowo mamy wbudowane pamięci podręczne pomiarów i kart układu. Dzięki temu złożoność wraca do O(n), co daje przewidywalną, liniową wydajność dla programistów stron internetowych. Jeśli jakiś układ ma układ 3-przebiegowy, również możemy zapisać go w pamięci podręcznej. Może to stwarzać możliwości bezpiecznego wprowadzenia bardziej zaawansowanych trybów układu w przyszłości. Ten przykład pokazuje, w jaki sposób funkcja RenderingNG odblokowuje możliwości rozszerzenia w całości. W niektórych przypadkach układ siatki może wymagać układu z 3 przejściami, ale obecnie zdarza się to niezwykle rzadko.
Okazuje się, że problemy z wydajnością związane z układem są zwykle spowodowane rosnącym wykładniczym błędem czasu układu, a nie nieprzetworzoną przepustowością etapu układu potoku. Jeśli mała zmiana (jeden element zmieniający jedną właściwość CSS) powoduje zmianę układu o 50–100 ms, prawdopodobnie jest to błąd układu o charakterze wykładniczym.
W skrócie
Układ jest bardzo złożoną kwestią, a my nie omówiliśmy wszystkich interesujących szczegółów, takich jak optymalizacja układu wstawianego (czyli w ogóle sposób działania podsystemu tekstowego), a nawet omówione tu zagadnienia stanowią tylko wierzchołek góry lodowej i nie wyczerpują wszystkich szczegółów. Mamy nadzieję, że udało nam się pokazać, jak systematyczne ulepszanie architektury systemu może prowadzić do nieproporcjonalnych zysków w długim okresie.
Zdajemy sobie jednak sprawę, że przed nami jeszcze wiele pracy. Zdajemy sobie sprawę z istnienia różnych klas problemów (zarówno związanych z wydajnością, jak i z poprawnością), nad którymi pracujemy. Cieszymy się też z nowych funkcji układu, które pojawią się w CSS. Wierzymy, że architektura LayoutNG sprawia, że rozwiązanie tych problemów jest bezpieczne i łatwe do rozwiązania.
Jedno zdjęcie (wiesz, które!) autorstwa Una Kravets.