Szczegółowa analiza renderowania: układNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Nazywam się Ian Kilpatrick. kierownikiem zespołu ds. układów Blink oraz Koji Ishii. Zanim zaczniesz pracę w zespole Blink, Byłem inżynierem interfejsu (zanim miałem rangę „inżyniera front-endu”), funkcji tworzenia w Dokumentach Google, Gmailu i na Dysku. Po około 5 latach na tym stanowisku podjąłem duże ryzyko, przenosząc się do zespołu Blink. efektywnej nauki języka C++ w pracy, i podejmujemy próby uruchomienia niezwykle złożonej bazy kodu Blink. Nawet do dziś udało mi się zrozumieć tylko niewielką część. Jestem wdzięczna za czas poświęcony w tym okresie. Pocieszyło mnie to, że wielu „odzyskiwanych inżynierów interfejsu” stał się „inżynierem przeglądarek” przed mną.

Moje wcześniejsze doświadczenia pomogły mi osobiście w pracy w zespole Blink. Jako inżynier frontu nieprzerwanie napotykam na niespójności w przeglądarkach, problemy z wydajnością, błędy renderowania i brakujące funkcje. Dzięki LayoutNG i stanowi sumę wyników 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ą.

Widok z 30 tys. metrów na architektury silnika układu

Wcześniej drzewo układu Blink było określane mianem „drzewa zmiennych”.

Wyświetla drzewo w sposób opisany w poniższym tekście.

Każdy obiekt w drzewie układu zawierał informacje wejściowe, takich jak dostępny rozmiar narzucony przez rodzica, położenie wszystkich liczb zmiennoprzecinkowych oraz informacje wyjściowe, na przykład ostatecznej szerokości i wysokości obiektu lub jego pozycji na osi x i y.

Obiekty te były wyświetlane między renderowaniem. Gdy nastąpiła zmiana stylu, i oznaczyliśmy go jako brudny, podobnie jak jego wszystkie obiekty nadrzędne na drzewie. Po uruchomieniu fazy układu potoku renderowania wyczyściliśmy drzewo, przeczyściliśmy wszystkie brudne obiekty i uruchomiliśmy układ, by je wyczyścić.

Zauważyliśmy, że taka architektura powoduje wiele klas problemów, które omówimy poniżej. Najpierw jednak cofniemy się i zastanowimy się, jakie dane wejściowe i wyjściowe są związane z układem.

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.

Opisany wcześniej model koncepcyjny.

Nasza nowa architektura sformalizuje 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, immutable obiekt o nazwie immutable.

Drzewo fragmentów.

Omówiliśmy wcześniejsze drzewo fragmentów niezmiennych, opisując, w jaki sposób wykorzystano w nim duże fragmenty poprzedniego drzewa w układach przyrostowych.

Dodatkowo przechowujemy nadrzędny obiekt ograniczeń, który wygenerował ten fragment. Jest on używany jako klucz pamięci podręcznej, który omówimy bardziej szczegółowo poniżej.

Algorytm układu wbudowanego (tekstowego) również został przepisany, aby pasował do nowej, stałej architektury. Nie tylko generuje stała reprezentacja płaskiej listy dla układu wbudowanego. Oferuje również buforowanie na poziomie akapitu, które przyspiesza przekazywanie, kształtu na akapit, aby stosować funkcje czcionek do elementów i słów, nowy dwukierunkowy algorytm Unicode korzystający z ICU, wiele poprawek poprawności i nie tylko.

Rodzaje błędów układu

Ogólnie błędy układów można podzielić na 4 kategorie: każdy z innych przyczyn.

Poprawność

Gdy myślimy o błędach w systemie renderowania, zwykle chodzi nam o poprawność, na przykład: „Przeglądarka A zachowuje się X, a przeglądarka B – Y”. lub „Przeglądarki A i B nie działają poprawnie”. Wcześniej poświęciliśmy wiele czasu na ten temat, i nieustannie walczyliśmy z systemem. Częstym trybem awarii było zastosowanie ściśle ukierunkowanej poprawki do jednego błędu. ale jednak odkryć, że po kilku tygodniach nastąpiła regresja w innej (pozornie niepowiązanej) części systemu.

Jak opisano w poprzednich postach, to oznaka bardzo niestabilnego systemu. Jeśli chodzi o układ, to nie mieliśmy przejrzystej umowy między żadnymi klasami, sprawia, że inżynierowie przeglądarek są uzależnieni od stanu, w którym nie powinni. lub błędnie zinterpretują jakąś wartość z innej części systemu.

Na przykład w pewnym momencie przez ponad rok mieliśmy łańcuch około 10 błędów. związane z układem elastycznym. Każda poprawka powodowała problem z poprawnością lub wydajnością systemu, prowadzi do kolejnego błędu.

Szablon LayoutNG wyraźnie definiuje umowę między wszystkimi komponentami w systemie układu, ale możemy też skuteczniej wprowadzać zmiany. Wiele skorzystamy też ze znakomitego projektu Web Platform Tests (WPT), który umożliwia wielu stronom udział we wspólnym zestawie testów internetowych.

Dziś okazuje się, że jeśli na kanale stabilnym wydamy prawdziwą regresję, zwykle nie ma powiązanych testów w repozytorium WPT, i nie wynika z niezrozumienia zawartych w nim umów. Zawsze dodajemy nowy test WPT, co pomaga zwiększyć pewność, że żadna przeglądarka nie powinna znowu popełniać tego samego błędu.

Zbyt mało unieważniona

Jeśli kiedykolwiek zdarzyło Ci się napotkać tajemniczy błąd polegający na magicznej zmianie rozmiaru okna przeglądarki lub przełączeniu właściwości CSS, na wypadek niedostatecznego unieważnienia. Część zmiennych drzewa została uznana za czysty, ale z powodu pewnych zmian w ograniczeniach nadrzędnych dane wyjściowe nie były prawidłowe.

Jest to bardzo częste w przypadku (dwukrotnie przechodząc po drzewie 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();
}

Poprawka tego typu błędu zazwyczaj wygląda tak:

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

Rozwiązanie tego typu problemu zwykle powodowałoby poważny spadek wydajności, (patrz o nadmierne unieważnienie poniżej), a jego poprawienie było bardzo delikatne.

Obecnie (jak opisano powyżej) mamy stały obiekt ograniczeń nadrzędnych, który opisuje wszystkie dane wejściowe z układu nadrzędnego do zasobu podrzędnego. Przechowujemy go wraz z wynikowym trwałym fragmentem. Z tego powodu mamy scentralizowane 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 nieprawidłowymi nieprawidłowymi kliknięciami zwykle wymaga ręcznego sprawdzenia 2 danych wejściowych i podejmując decyzję o zmianie w danych wejściowych, tak aby konieczne było przesłanie kolejnego przejścia układu.

Poprawki kodu różnicowego są zwykle proste, i łatwe testowanie jednostkowe ze względu na prostotę tworzenia tych niezależnych obiektów.

Porównanie obrazu o stałej i procentowej szerokości.
W przypadku elementu o stałej szerokości i wysokości nie ma znaczenia, że podany mu dostępny rozmiar się zwiększy, ale wyrażona w procentach szerokość/wysokość już tak. Wartość available-size jest reprezentowana w obiekcie Ograniczenia nadrzędne i w ramach algorytmu różnicowania przeprowadza tę optymalizację.

Kod różnicowy w powyższym przykładzie to:

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

Histereza

Ta klasa błędów jest podobna do nieuprawnionego unieważniania. Zasadniczo w poprzednim systemie niezmiernie trudno było dopilnować, żeby układ był idempotentny – tzn. ponowne uruchomienie układu z tymi samymi danymi wejściowymi dało te same dane wyjściowe.

W przykładzie poniżej zmieniamy po prostu między dwiema wartościami właściwości CSS. Efektem jest jednak „nieskończony wzrost”. prostokąta.

Film i prezentacja przedstawiają błąd związany z histerezą w Chrome 92 i starszych wersjach. Jest on naprawiony w Chrome 93.

W naszym poprzednim drzewie zmiennym ale łatwo było wprowadzić takie błędy. Jeśli kod popełnił błąd, odczytując rozmiar lub położenie obiektu w niewłaściwym czasie lub na nieprawidłowym etapie (ponieważ na przykład nie wyczyściliśmy poprzedniego rozmiaru lub pozycji) od razu dodamy subtelny błąd histerezy. Takie błędy zwykle nie pojawiają się w testach, ponieważ większość z nich skupia się na pojedynczym układzie i renderowaniu. Co bardziej niepokojące, zdawaliśmy sobie sprawę, że do prawidłowego działania niektórych trybów układu konieczna jest pewna histereza. Mieliśmy błędy w optymalizacji, by usunąć kartę układu, ale dodaj „robaka” ponieważ tryb układu wymaga 2 przebiegów, aby uzyskać prawidłowe dane wyjściowe.

Drzewo przedstawiające problemy opisane we wcześniejszym tekście.
W zależności od poprzednich informacji o wynikach układu powstaje układ nieidempotentny

Ze względu na konkretne struktury danych wejściowych i wyjściowych w LayoutNG i nie można uzyskać dostępu do poprzedniego stanu, udało nam się wyeliminować tę klasę błędów z systemu układu.

Nadmierna liczba nieprawidłowego ruchu i skuteczność

Jest to bezpośrednie przeciwieństwo klasy błędów, które nie pozwalają na ustalenie zasad. Naprawianie błędu związanego z nieprawidłowym naruszeniem zasad często powodowało spadek wydajności.

Często musieliśmy dokonywać trudnych wyborów, faworyzować poprawność, a nie skuteczność. W następnej sekcji bardziej szczegółowo opowiemy, jak udało nam się wyeliminować tego typu problemy z wydajnością.

Narodziny układu dwuprzepustowego i wysokich wydajności

Układy elastyczny i siatkowy reprezentowały zmianę ekspresji układów w internecie. Jednak te algorytmy zasadniczo różniły się od poprzedniego algorytmu układu blokowego.

Układ blokowy (w prawie wszystkich przypadkach) wymaga, by silnik dokładnie raz uwzględnił układ względem wszystkich swoich elementów podrzędnych. Świetnie sprawdza się to w przypadku wydajności, ale okazuje się, że nie będzie on tak ekspresyjny, jakiego życzą sobie programiści stron internetowych.

Przykład: często chcesz, aby wszystkie elementy podrzędne rozwijały się do rozmiaru największego. Żeby to umożliwić, układ nadrzędny (elastyczny lub siatka) a następnie wykona pomiar, aby określić, jak duże jest każde z dzieci, a następnie karnet 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.

2 zestawy pól. Pierwszy pokazuje wewnętrzny rozmiar pól w przejściu miarowym, a drugi na układzie równym wysokości.

Te dwuprzebiegowe układy były początkowo akceptowalne ze względu na wydajność, bo ludzie zwykle nie kładą się w nich głęboko. W miarę pojawiania się bardziej złożonych treści zaczęliśmy jednak zauważyć poważne problemy z wydajnością. Jeśli nie zapiszesz wyniku etapu pomiaru w pamięci podręcznej, drzewo układu przechodzi między stanem pomiaru a końcowym stanem układu.

Układy jedno-, dwu- i trzyprzepustowe opisane w napisach.
Powyższy obraz przedstawia 3 elementy <div>. Prosty układ jednoprzebiegowy (np. układ blokowy) odwiedza 3 węzły układu (złożoność O(n). Jednak w przypadku układu dwuprzebiegowego (np. Flex lub siatki) może to spowodować złożoność wizyt O(2n) w tym przykładzie.
Wykres przedstawiający wykładniczy wzrost czasu układu.
Na tej ilustracji i w wersji demonstracyjnej przedstawiono układ wykładniczy z układem siatki. Ten błąd został naprawiony w Chrome 93 w wyniku przeniesienia siatki do nowej architektury.

Wcześniej próbowaliśmy dodawać bardzo konkretne pamięci podręczne do elastycznego układu i układu siatki, by rozwiązać ten problem. To się sprawdziło (i dzięki Flex zaszliśmy bardzo daleko), ale nieustannie walczymy z wieloma błędami unieważniania.

LayoutNG pozwala tworzyć wyraźne struktury danych zarówno dla danych wejściowych, jak i wyjściowych układu, Utworzyliśmy także pamięci podręczne dla pomiarów i kart układu. Sprawia to, że złożoność powraca do O(n), co w przypadku twórców stron internetowych – przewidywalnie liniowo. Jeśli jakiś układ ma układ 3-przebiegowy, również możemy zapisać go w pamięci podręcznej. Może to dać Ci szansę na bezpieczne wprowadzenie bardziej zaawansowanych trybów układu w przyszłości. Ten przykład pokazuje, w jaki sposób RenderingNG. rozszerzają możliwości rozszerzenia w całym obszarze. W niektórych przypadkach układ siatki może wymagać układu z 3 przejściami, ale obecnie zdarza się to niezwykle rzadko.

Gdy deweloperzy mają problemy z wydajnością dotyczące układu strony, jest zwykle spowodowane błędem układu wykładniczego, a nie z pełną przepustowością etapu układu potoku. Jeśli mała zmiana przyrostowa (jeden element zmieniający pojedynczą właściwość CSS) spowoduje wyświetlenie układu 50–100 ms, prawdopodobnie jest to błąd układu wykładniczego.

W skrócie

Układ to niezwykle złożony obszar, i nie omówiliśmy wszystkich interesujących szczegółów, takich jak optymalizacja układu wbudowanego. (jak działa cały podsystem wbudowany i tekstowy) a nawet omawiane tu pojęcia wykazywały jedynie wierzchołek góry lodowej, i przepłynęło wiele szczegółów. Mamy jednak nadzieję, że udało nam się wykazać, jak systematyczne ulepszanie architektury systemu może prowadzić do niesamowitych korzyści w dłuższej perspektywie.

Zdajemy sobie jednak sprawę, że przed nami jeszcze wiele pracy. Jesteśmy świadomi klas problemów (zarówno dotyczących skuteczności, jak i poprawności), nad którymi pracujemy, i są zadowoleni 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.

Jeden obraz (wiesz, który!) Uny Kravets.