Szczegółowa analiza renderowania: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Termin Blink odnosi się do implementacji platformy internetowej w Chromium i obejmuje wszystkie fazy renderowania przed kompozycją, aż do uzyskania zatwierdzenia kompozytora. Więcej informacji o architekturze renderowania mruga znajdziesz w poprzednim artykule z tej serii.

Blink narodził się jako rozwidlenie WebKit, który sam w sobie jest rozwidleniem języka KHTML, którego historia sięga 1998 roku. Zawiera jeden z najstarszych (i najbardziej krytycznych) kodów w Chromium, a do 2014 roku już zdecydowanie pokazał swój wiek. W tym roku rozpoczęliśmy zbiór ambitnych projektów pod hasłem „BlinkNG”, których celem było usunięcie długotrwałych niedoskonałości w organizacji i strukturze kodu Blink. W tym artykule omawiamy projekty BlinkNG i jego elementy składowe: dlaczego je podjęliśmy, jakie osiągnęliśmy, a także zasady, które ukształtowały tę platformę, oraz możliwości przyszłych ulepszeń.

Potok renderowania przed BlinkNG i po nim.

Renderowanie przed optymalizacją

Potok renderowania w Blink zawsze był koncepcyjnie podzielony na fazy (styl, układ, malowanie itd.), ale bariery abstrakcji były niewidoczne. Ogólnie rzecz biorąc, dane związane z renderowaniem składały się z trwałych, zmiennych obiektów. Obiekty te mogły być w każdej chwili modyfikowane oraz wykorzystywane ponownie w ramach kolejnych aktualizacji renderowania. Nie można było rzetelnie odpowiedzieć na proste pytania, takie jak:

  • Czy dane wyjściowe stylu, układu lub renderowania trzeba zaktualizować?
  • Kiedy te dane staną się „ostateczne”? wartość?
  • Kiedy można te dane zmodyfikować?
  • Kiedy ten obiekt zostanie usunięty?

Jest wiele takich przykładów:

Style generowałyby elementy typu ComputedStyle na podstawie arkuszy stylów. ale nie można zmienić wartości ComputedStyle; w niektórych przypadkach może zostać zmodyfikowany przez późniejsze etapy potoku.

Użycie opcji Style spowoduje wygenerowanie drzewa LayoutObject, a następnie użycie właściwości layout za pomocą adnotacji o rozmiarze i pozycjonowaniu. W niektórych przypadkach parametr układ zmodyfikuje nawet strukturę drzewa. Nie było wyraźnego rozdzielenia między danymi wejściowymi a wyjściowymi układu.

Opcja Styl generowałaby struktury danych akcesoriów określających przebieg komponowania. Te struktury danych były modyfikowane w każdej fazie po zakończeniu stylu.

Na niższym poziomie typy danych renderowania składają się w dużej mierze z wyspecjalizowanych drzew (np. drzewa DOM, drzewo stylów, drzewa układu, drzewa właściwości renderowania). a fazy renderowania – rekurencyjne spacery po drzewie. W idealnym przypadku spacer po drzewie powinien być zawierający: podczas przetwarzania danego węzła drzewa nie należy uzyskiwać dostępu do żadnych informacji spoza drzewa podrzędnego, które znajduje się w tym węźle. To wcześniej nie było możliwe podczas wstępnego renderowania. spacery po drzewie często uzyskują dostęp do informacji z elementów nadrzędnych przetwarzanego węzła. Dzięki temu system stał się bardzo delikatny i podatny na błędy. Nie dało się też rozpocząć spaceru z wyjątkiem korzenia drzewa.

W kodzie procesu renderowania pojawiło się też wiele wejść: wymuszone uruchamianie układów przez JavaScript, częściowe aktualizacje aktywowane podczas wczytywania dokumentu, wymuszenie aktualizacji w celu przygotowania kierowania na zdarzenia, zaplanowane aktualizacje żądane przez system wyświetlania oraz specjalistyczne interfejsy API udostępniane tylko do testowania kodu. W potoku renderowania pojawiło się nawet kilka ścieżek rekurencyjnych i powracających (czyli przechodzenie na początek jednego etapu ze środka drugiego). Każda z tych ramp ma swoje własne zachowanie, a w niektórych przypadkach wynik renderowania będzie zależał od sposobu aktualizacji renderowania.

Co się zmieniło

BlinkNG składa się z wielu podprojektów, zarówno dużych, jak i małych, których celem jest wyeliminowanie opisanych wcześniej deficytów architektonicznych. W projektach tych opiera się kilka głównych zasad, które mają sprawić, że potok renderowania będzie pełnił w rzeczywistości:

  • Jednolity punkt wejścia: potok należy zawsze wprowadzać na początku.
  • Etapy funkcyjne: każdy etap powinien mieć dobrze zdefiniowane dane wejściowe i wyjściowe, a jego działanie powinno być funkcjonalne, czyli deterministyczne i powtarzalne, a wyniki powinny zależeć tylko od zdefiniowanych danych wejściowych.
  • Stałe dane wejściowe: gdy etap jest uruchomiony, dane wejściowe dowolnego etapu powinny być stałe.
  • Stałe dane wyjściowe: po zakończeniu etapu jego dane wyjściowe powinny być niezmienne przez resztę czasu aktualizacji renderowania.
  • Spójność punktów kontrolnych: na końcu każdego etapu uzyskane do tej pory dane renderowania powinny być niespójne.
  • Zminimalizowanie pracy: każdy element jest obliczany tylko raz.

Pełna lista podprojektów BlinkNG może być męcząca, ale poniżej przedstawiamy kilka szczególnej konsekwencji.

Cykl życia dokumentu

Klasa DocumentLifecycle śledzi postęp renderowania. Pozwala nam to przeprowadzać podstawowe testy, które egzekwują wymienione wcześniej niezmienniki, na przykład:

  • Jeśli modyfikujemy właściwość ComputedStyle, cykl życia dokumentu musi wynosić kInStyleRecalc.
  • Jeśli stan DocumentLifecycle to kStyleClean lub później, funkcja NeedsStyleRecalc() musi zwracać wartość false dla każdego podłączonego węzła.
  • Gdy wchodzisz na etap cyklu życia malowania, stan cyklu życia musi wynosić kPrePaintClean.

W trakcie wdrażania BlinkNG systematycznie eliminowaliśmy ścieżki naruszające te niezmienniki, a także umieszczaliśmy w całym kodzie znacznie więcej asercji, by zapobiegać występowaniu regresji.

Jeśli zdarzyło Ci się zobaczyć niskopoziomowy kod renderujący w królinowej jaskini, możesz zadać sobie pytanie: „Skąd się tu wzięłam?”. Jak już wspomnieliśmy, potok renderowania można trafiają do różnych punktów wejścia. Wcześniej obejmowały one ścieżki rekurencji i powracających rozmów oraz miejsca, w których wchodziliśmy do potoku na etapie przejściowym, a nie na początku. W trakcie szkolenia BlinkNG przeanalizowaliśmy te ścieżki połączeń i ustaliliśmy, że można je zredukować do 2 podstawowych scenariuszy:

  • Wszystkie dane renderowania muszą być aktualizowane, np. podczas generowania nowych pikseli na potrzeby wyświetlania lub testowania działań w przypadku kierowania na zdarzenia.
  • Potrzebujemy aktualnej wartości dla konkretnego zapytania, na które można odpowiedzieć bez aktualizowania wszystkich danych renderowania. Obejmuje to większość zapytań JavaScript, np. node.offsetTop.

W odniesieniu do tych 2 scenariuszy istnieją teraz tylko 2 punkty wejścia do potoku renderowania. Ścieżki kodu reententa zostały usunięte lub zmodyfikowane. Nie można już wejść do potoku, zaczynając od fazy pośredniej. Dzięki temu nie trzeba zastanawiać się nad tym, kiedy i w jaki sposób przeprowadzana jest aktualizacja renderowania, co znacznie ułatwia analizę działania systemu.

Styl, układ i wstępne malowanie potoku

Etapy renderowania przed wyrenderowaniem łącznie odpowiadają za:

  • Użycie algorytmu kaskady stylów do obliczenia końcowych właściwości stylu dla węzłów DOM.
  • Generuję drzewo układu reprezentujące hierarchię pól dokumentu.
  • Ustalam informacje o rozmiarze i położeniu dla wszystkich pól.
  • Zaokrąglanie lub przyciąganie geometrii subpikselowej do obramowań całych pikseli na potrzeby malowania.
  • Określanie właściwości skomponowanych warstw (przekształcenia afinansowego, filtrów, nieprzezroczystości lub innych elementów, które mogą być przyspieszane za pomocą GPU).
  • określenie, które treści uległy zmianie od poprzedniego etapu wyrenderowania i które trzeba wymalować lub odmalować ponownie (unieważnienie renderowania).

Ta lista nie uległa zmianie, ale przed BlinkNG sporą część tych prac przeprowadzono doraźnie, z uwzględnieniem wielu faz renderowania, z wieloma zduplikowanymi funkcjami i wbudowanymi problemami. Na przykład etap stylu odpowiadał głównie za obliczanie ostatecznych właściwości stylu dla węzłów, ale w pewnych wyjątkowych przypadkach określaliśmy ostateczne wartości właściwości stylu dopiero po zakończeniu fazy stylu. W procesie renderowania nie było żadnego formalnego ani możliwego do wyegzekwowania punktu, w którym moglibyśmy z całą pewnością stwierdzić, że informacje o stylu są kompletne i niezmienne.

Innym dobrym przykładem problemu przed BlinkNG jest unieważnienie renderowania. Wcześniej to ustawienie występowało na wszystkich etapach renderowania przed wyrenderowaniem. Podczas modyfikowania stylu lub kodu układu trudno było określić, jakie zmiany były wymagane w procesie unieważniania renderowania. Łatwo też było popełnić błąd, który doprowadził do zaniżania lub nadmiernej liczby nieprawidłowych wyświetleń. Więcej informacji o starym systemie unieważniania renderowania znajdziesz w artykule z serii na temat LayoutNG.

Przyciągnięcie geometrii układu subpikselowego do granic całych pikseli na potrzeby malowania to przykład sytuacji, w której mieliśmy wiele implementacji tych samych funkcji i wykonaliśmy mnóstwo nadmiarowych zadań. System renderowania wykorzystywał jedną ścieżkę kodu do pobierania pikseli, a gdy trzeba było na bieżąco obliczać współrzędne pikselowe, poza kodem renderowania wykorzystywana była całkowicie osobna ścieżka. Nie trzeba dodawać, że każda implementacja miała własne błędy, a wyniki nie zawsze się zgadzały. Ze względu na to, że te informacje nie były zapisywane w pamięci podręcznej, system czasami powtarzał te same obliczenia, co ponownie zmniejszało wydajność.

Oto kilka ważnych projektów, które wyeliminowały deficyty architektoniczne fazy renderowania poprzedzającego malowanie.

Project Squad: utrwalanie fazy stylu

Ten projekt pokonał 2 główne deficyty w fazie projektowania, które uniemożliwiły jego realizację:

Istnieją 2 główne dane wyjściowe fazy stylu: ComputedStyle, które zawierają wynik uruchomienia algorytmu kaskadowego CSS w drzewie DOM. i drzewo LayoutObjects określające kolejność działań dla fazy układu. Algorytm kaskadowy powinien przede wszystkim następować bezpośrednio przed wygenerowaniem drzewa układu. ale wcześniej te 2 operacje były przeplatane. Projektowi Project Squad udało się podzielić te dwie fazy na osobne, sekwencyjne.

Wcześniej wartość końcowa ComputedStyle nie zawsze była uzyskiwana podczas ponownego obliczania stylu. wystąpiło kilka sytuacji, w których zasób ComputedStyle został zaktualizowany na późniejszym etapie potoku. Zespół Project Squad zrefaktoryzował te ścieżki kodu, dzięki czemu element ComputedStyle nigdy nie jest modyfikowany po etapie stylu.

LayoutNG: tworzenie fazy układu

Ten monumentalny projekt, który jest jednym z podstawowych elementów RenderingNG, stanowił całkowite przeredagowanie fazy renderowania układu. Nie przedstawimy całego projektu BlinkNG, ale można wyróżnić kilka aspektów całego projektu BlinkNG:

  • Wcześniej etap układu otrzymał drzewo LayoutObject utworzone przez etap stylu z adnotacjami o rozmiarze i pozycji. Dzięki temu nie udało się wyraźnie oddzielić danych wejściowych od danych wyjściowych. Wprowadzono drzewo fragmentów, które stanowi podstawowe dane wyjściowe układu, tylko do odczytu, i służy jako podstawowe dane wejściowe w kolejnych fazach renderowania.
  • Funkcja LayoutNG wprowadziła właściwość elementu do układu: podczas obliczania rozmiaru i pozycji danego elementu LayoutObject nie weryfikujemy już drzewa podrzędnego umieszczonego w jego korzenie. Wszystkie informacje potrzebne do zaktualizowania układu danego obiektu są obliczane wcześniej i dostarczane algorytmowi jako dane wejściowe tylko do odczytu.
  • Wcześniej w niektórych przypadkach algorytm układu nie działał całkowicie – jego wynik zależał od ostatniej aktualizacji układu. Usługa LayoutNG wyeliminowała te przypadki.

Faza wstępnego renderowania

Wcześniej nie było formalnego etapu renderowania wstępnym, a jedynie wykonać kilka czynności po wykonaniu procesu. Faza wstępnego wyrenderowania rozwijała się, ponieważ istnieje kilka powiązanych funkcji, które najlepiej wdrożyć w postaci systematycznego przemierzania drzewa układu po ukończeniu układu. co najważniejsze:

  • Nakładanie unieważnienia renderowania: gdy mamy niepełne informacje, bardzo trudno jest prawidłowo przeprowadzić unieważnienie renderowania w układzie. Znacznie ułatwia to prawidłowe i może być bardzo efektywne, jeśli dzieli się na 2 różne procesy: w przypadku określania stylu i układu treść można oznaczyć za pomocą prostej flagi wartości logicznej, jako że „prawdopodobnie wymaga unieważnienia renderowania”. Podczas wstępnego malowania drzew sprawdzamy te flagi i w razie potrzeby je unieważniamy.
  • Generowanie drzew właściwości farb: proces opisany bardziej szczegółowo w dalszej części tego artykułu.
  • Obliczanie i rejestrowanie pikselowych wyrenderowanych lokalizacji: zarejestrowane wyniki mogą być wykorzystywane zarówno w fazie renderowania, jak i w dowolnym kodzie podrzędnym, bez konieczności wykonywania dodatkowych obliczeń.

Drzewa właściwości: spójna geometria

Drzewa właściwości zostały wprowadzone na początku w RenderingNG, aby rozwiązać problem złożoności przewijania, które w internecie ma inną strukturę niż pozostałe rodzaje efektów wizualnych. Przed drzewami właściwości kompozytor Chromium używał pojedynczej „warstwy” do reprezentowania zależności geometrycznych komponowanych treści, ale szybko się one rozpadły w miarę złożoności funkcji, takich jak pozycja:stała. W hierarchii warstw pojawiły się dodatkowe, nielokalne wskaźniki wskazujące na element nadrzędny przewijania. lub „Clip nadrzędny” i już wkrótce było bardzo trudno zrozumieć kod.

Drzewa właściwości naprawiły to, przedstawiając aspekt przewinięcia i klipów treści oddzielnie od wszystkich innych efektów wizualnych. Pozwoliło to prawidłowo modelować prawdziwą strukturę wizualną i przewijaną stron internetowych. Następne: „wszystko” musieliśmy wdrożyć algorytmy na drzewie właściwości, takie jak przekształcenie przestrzeni ekranu skomponowanych warstw lub określenie, które warstwy się przewijały, a które nie.

Szybko okazało się, że w kodzie jest wiele innych miejsc, w których pojawiają się podobne pytania geometryczne. Pełną listę znajdziesz w poście na temat kluczowych struktur danych. Część z nich miała zduplikowane implementacje tego samego działania co kod kompozytora. każdy miał inny podzbiór błędów, i żaden z nich nie odwzorował prawdziwej struktury witryny. Rozwiązanie stało się wtedy jasne: scentralizuj wszystkie algorytmy geometryczne w jednym miejscu i przeprowadź refaktoryzację całego kodu, aby z niego korzystać.

Algorytmy te z kolei bazują na drzewach właściwości, dlatego stanowią one kluczową strukturę danych – wykorzystywaną w procesie renderowania RenderingNG. Aby osiągnąć ten cel związany ze scentralizowanym kodem geometrycznym, musieliśmy wprowadzić koncepcję drzew właściwości znacznie wcześniej w procesie wyrenderowania – i przed wyrenderowaniem – i zmienić wszystkie interfejsy API, które od nich wymagały, tak aby wymagały uruchomienia wstępnego wyrenderowania.

Ta historia to kolejny aspekt wzorca refaktoryzacji BlinkNG: zidentyfikowanie kluczowych obliczeń, refaktoryzacja w celu uniknięcia ich powielania i tworzenie dobrze zdefiniowanych etapów potoku, które tworzą struktury danych dostarczające te dane. Drzewa nieruchomości obliczamy dokładnie w chwili, gdy dostępne są wszystkie niezbędne informacje. Zapewniamy też, że drzewa właściwości nie mogą się zmienić podczas późniejszych etapów renderowania.

Kompozyt po farbowaniu: farba do rur i komponowanie

Warstwa to proces określania, która treść DOM staje się własną warstwą skomponowaną (która z kolei reprezentuje teksturę GPU). Przed renderowaniem NG tworzenie warstw było uruchamiane przed wyrenderowaniem, a nie po nim (tutaj znajdziesz informacje o bieżącym potoku – zwróć uwagę na zmianę kolejności). Najpierw określimy, które części DOM trafią do którejś warstwy skomponowanej, a następnie narysujemy listy wyświetlania dla tych tekstur. Decyzje te zależą od czynników takich jak to, które elementy DOM były animowane lub przewijały albo miały przekształcenia 3D oraz które elementy zostały na nich namalowane.

Było to przyczyną poważnych problemów, ponieważ w kodzie znajdowały się zależności cykliczne, co stanowi poważny problem dla potoku renderowania. Przyjrzyjmy się przykładowi. Załóżmy, że musimy unieważnić wyrenderowanie (co oznacza, że musimy ponownie narysować listę wyświetlania, a następnie ponownie ją rastrować). Konieczność unieważnienia może wynikać ze zmiany w DOM bądź ze zmiany stylu lub układu. Oczywiście unieważnimy tylko te części, które faktycznie się zmieniły. Trzeba było sprawdzić, których warstw skomponowano, a następnie unieważnić części lub wszystkie wyświetlane listy dla tych warstw.

Oznacza to, że unieważnienie zależy od DOM, stylu, układu i wcześniejszych decyzji dotyczących warstw (wcześniej: znaczenie poprzedniej renderowanej klatki). Jednak obecne warstwy zależą także od wszystkich tych czynników. A ponieważ nie mieliśmy 2 kopii wszystkich danych nakładania warstw, trudno było rozróżnić wcześniejsze i przyszłe decyzje dotyczące warstw. W końcu otrzymaliśmy kod, który opiera się na zapętlonym rozumowaniu. Czasami prowadziło to do złego lub nielogicznego kodu, a nawet awarii lub problemów z bezpieczeństwem, jeśli nie robiliśmy tego zbyt ostrożnie.

Aby sobie z tym poradzić, na początku wprowadziliśmy pojęcie obiektu DisableCompositingQueryAsserts. W większości przypadków, gdy kod próbował wysłać zapytania do decyzji dotyczących wklejania warstw, powodowałoby to niepowodzenie asercji i awarię przeglądarki, jeśli była ona w trybie debugowania. Dzięki temu uniknęliśmy wprowadzania nowych błędów. W każdym przypadku, gdy kod był w uzasadniony sposób potrzebny do sprawdzenia decyzji dotyczących wklejania warstw, umieszczamy kod, który zezwala na to, przydzielając obiekt DisableCompositingQueryAsserts.

Naszym planem było pozbycie się z czasem wszystkich obiektów DisableCompositingQueryAssert do obsługi wywołań, a następnie zadeklarowanie, że kod jest bezpieczny i prawidłowy. Odkryliśmy jednak, że usunięcie wielu wywołań było w zasadzie niemożliwe, o ile przed wyrenderowaniem treści nastąpiło nakładanie warstw. (ostatnio niedawno udało nam się je usunąć). Jest to pierwszy powód, dla którego odkryto projekt Composite After Paint. Dowiedzieliśmy się, że nawet jeśli masz dobrze zdefiniowany etap potoku dla danej operacji, to jeśli znajduje się on w niewłaściwym miejscu potoku, w końcu utkniesz.

Drugim powodem utworzenia projektu Composite After Paint był błąd dotyczący komponowania podstawowego. Jednym ze sposobów na wskazanie tego błędu jest to, że elementy DOM nie są dobrą reprezentacją 1:1 efektywnego lub kompletnego schematu warstwowego zawartości stron internetowych. A ponieważ komponowanie było przed wyrenderowaniem, z założenia zależało w mniejszym lub większym stopniu od elementów DOM, a nie od list wyświetlania czy drzew właściwości. Jest to bardzo podobne do powodów, dla których wprowadziliśmy drzewa właściwości – tak jak w przypadku drzew właściwości, rozwiązanie wypada bezpośrednio, jeśli uda Podobnie jak w przypadku drzew właściwości stanowiła dobrą okazję do zagwarantowania, że po zakończeniu fazy malowania dane wyjściowe są stałe dla wszystkich kolejnych faz potoku.

Zalety

Jak widać, dobrze zdefiniowany potok renderowania przynosi ogromne korzyści w dłuższej perspektywie. Jest jeszcze więcej, niż mogłoby się wydawać:

  • Znacznie większa niezawodność: to jest całkiem proste. Bardziej przejrzysty kod z dobrze zdefiniowanymi i zrozumiałymi interfejsami jest łatwiejszy do zrozumienia, pisania i testowania. Dzięki temu jest to bardziej niezawodne. Zwiększa też bezpieczeństwo i stabilność kodu, ograniczając liczbę awarii i błędów, które nie są niezbędne.
  • Rozszerzony zasięg testów: w trakcie BlinkNG dodaliśmy do naszego pakietu wiele nowych testów. Obejmuje to testy jednostkowe, które umożliwiają ukierunkowaną weryfikację pracowników wewnętrznych. testy regresji, które uniemożliwiają ponowne wprowadzenie starych błędów, które już naprawiliśmy (ogromnie jest ich wiele); oraz wiele dodatkowych ulepszeń, wspólnie aktualizowanych w ramach pakietu Web Platform Test Suite, którego wszystkie przeglądarki używają do pomiaru zgodności ze standardami internetowymi.
  • Łatwiejsze rozszerzanie: jeśli system jest podzielony na przejrzyste komponenty, nie musisz szczegółowo poznawać innych elementów, by móc robić postępy w bieżącym. Dzięki temu każdy użytkownik może wnieść wartość do kodu renderowania bez konieczności dogłębnej wiedzy na ten temat. Pozwala to również wyciągać wnioski na temat działania całego systemu.
  • Wydajność: optymalizacja algorytmów napisanych w kodzie spaghetti jest dość trudna, ale bez takiego potoku osiągnięcie jeszcze większych rzeczy, takich jak uniwersalne przewijanie w wątkach i animacje, oraz procesy i wątki do izolacji witryn jest niemal niemożliwe. Równoległość może znacznie zwiększyć wydajność, ale jest też niezwykle skomplikowany.
  • Ustąpienie i ograniczenie: BlinkNG wprowadził kilka nowych funkcji, które wykorzystują ten potok na nowe i nowatorskie sposoby. Co na przykład, jeśli chcemy uruchamiać potok renderowania tylko do czasu wygaśnięcia budżetu? A może pominąć renderowanie w przypadku drzew podrzędnych, o których użytkownik nie wie, że są w tej chwili nieistotne? Pozwala to na właściwość CSS content-visibility. Co w przypadku decydowania o stylu komponentu zależnie od jego układu? Będą to zapytania dotyczące kontenerów.

Studium przypadku: zapytania dotyczące kontenerów

Zapytania dotyczące kontenerów to długo oczekiwana funkcja platformy internetowej, która od lat jest najczęściej żądaną przez deweloperów usług porównywania cen. Skoro jest taki świetny, dlaczego jeszcze go nie ma? Dzieje się tak, ponieważ implementacja zapytań dotyczących kontenera wymaga bardzo dokładnego zrozumienia i kontrolowania relacji między stylem a kodem układu. Przyjrzyjmy się temu bliżej.

Zapytanie o kontener umożliwia stosowanie stylów, które mają zastosowanie do elementu, w zależności od określonego rozmiaru elementu nadrzędnego. Rozmiar układu jest obliczany w trakcie układu, co oznacza, że po jego zakończeniu musimy ponownie wygenerować styl. ale recalc stylu działa przed układem. Ten paradoks dotyczący kurczaka i jajka stanowi właśnie powód, dla którego nie mogliśmy wdrożyć zapytań dotyczących kontenera przed BlinkNG.

Jak rozwiązać ten problem? Czy nie jest to zależność wsteczna potoku, czyli ten sam problem, który rozwiązano w projektach takich jak Composite After Paint? Co gorsza, co się stanie, jeśli nowe style zmienią rozmiar elementu nadrzędnego? Czy nie spowoduje to czasami nieskończonej pętli?

Zależność cykliczną można rozwiązać za pomocą właściwości „zawiera” CSS, która umożliwia renderowanie poza elementem, bez uwzględniania renderowania w drzewie tego elementu. To oznacza, że nowe style stosowane przez kontener nie mają wpływu na jego rozmiar, ponieważ zapytania dotyczące kontenera wymagają izolacji.

W rzeczywistości to jednak nie wystarczyło i konieczne było wprowadzenie słabszego rodzaju izolacji niż tylko rozmiar. Dzieje się tak, ponieważ często zależy nam, aby kontener zapytań o kontener mógł zmieniać rozmiar tylko w jednym kierunku (zwykle jest to blok) na podstawie wymiarów wbudowanych. Dlatego dodaliśmy koncepcję ograniczenia rozmiaru wewnętrznego. Jak widać jednak w bardzo długiej notatce w tej sekcji, przez długi czas nie było jasne, czy można było ograniczyć rozmiar w tekście.

Czym jest opisywanie ograniczeń treści w abstrakcyjnym języku specyficznym, a prawidłową implementacją – zupełnie inną. Trzeba pamiętać, że jednym z celów programu BlinkNG było wprowadzenie zasady izolacji w drzewa, które stanowią główną logikę renderowania: podczas przemierzania poddrzewa nie są wymagane żadne informacje spoza poddrzewa. Tak się składa, że nie jest to przypadkowy kod, ale kod jest znacznie czystszy i łatwiejszy do wdrożenia zabezpieczenia CSS, jeśli kod renderowania jest zgodny z zasadą izolacji.

Przyszłość: komponowanie poza głównym wątkiem... i nie tylko!

Potok renderowania widoczny tutaj jest nieco późniejszy niż bieżąca implementacja RenderingNG. Pokazuje ono warstwy warstw jako wyłączone z głównego wątku, podczas gdy będzie ono nadal widoczne w wątku głównym. Jest to jednak tylko kwestią czasu, ponieważ teraz, po wyrenderowaniu Kompozytu po wyrenderowaniu, a nałożenie warstw jest już po farbowaniu.

Aby zrozumieć, dlaczego to takie ważne i do czego jeszcze może prowadzić, trzeba przyjrzeć się architekturze silnika renderowania z większej perspektywy. Jedną z najbardziej trwałych przeszkód w poprawie wydajności Chromium jest fakt, że główny wątek mechanizmu renderowania obsługuje zarówno główną logikę aplikacji (czyli uruchomiony skrypt), jak i znaczną część renderowania. W efekcie główny wątek jest często przepełniony pracą, a przeciążenie wątku jest często wąskim gardłem całej przeglądarki.

Dobra wiadomość jest taka, że nie musi tak być! Ten aspekt architektury Chromium sięga czasów KHTML, kiedy dominującym modelem programowania było uruchamianie w jednym wątku. Gdy procesory wielordzeniowe stały się powszechnym w urządzeniach konsumenckich, zasada jednowątkowa została mocno zakorzeniona w Blink (wcześniej WebKit). Już od dawna chcialiśmy wprowadzić do mechanizmu renderowania więcej wątków, ale w starym systemie było to po prostu niemożliwe. Jednym z głównych celów renderowania NG było wydobycie się z tej dziury i umożliwienie przeniesienia procesu renderowania, w części lub w całości, do innego wątku (lub wątków).

BlinkNG zbliża się do końca, więc zaczynamy już badać ten obszar. Zatwierdzenie Non-block Commit to pierwszy krok do zmiany modelu wątków mechanizmu renderowania. Zatwierdzenie kompozytora (lub po prostu commit) to krok synchronizacji między wątkiem głównym a wątkiem kompozytora. Podczas zatwierdzania tworzymy kopie danych renderowania generowane w wątku głównym do wykorzystania przez dalszy kod kompozycyjny uruchomiony w wątku kompozytora. Podczas synchronizacji wykonywanie wątku głównego jest zatrzymywane, a kopiowanie kodu działa w wątku kompozytora. Dzięki temu wątek główny nie modyfikuje danych renderowania w czasie, gdy wątek kompozytora jest kopiowany.

Zatwierdzenie nieblokujące eliminuje konieczność zatrzymania wątku głównego i oczekiwania na zakończenie etapu zatwierdzania. Wątek główny będzie nadal działać, podczas gdy zatwierdzenie będzie uruchamiane równocześnie w wątku kompozytora. Efektem netto zatwierdzenia nieblokowania będzie skrócenie czasu poświęcanego na renderowanie w wątku głównym, co zmniejszy zatłoczenie w wątku głównym i zwiększy wydajność. Na tę chwilę (marzec 2022 r.) mamy działający prototyp zatwierdzenia nieblokowania. Pracujemy nad szczegółową analizą jego wpływu na wydajność.

Oczekiwanie w skrzydłach to komponowanie poza głównym wątkiem, którego celem jest dopasowanie silnika renderującego do ilustracji przez przeniesienie warstw z wątku głównego na wątek roboczy. Podobnie jak w przypadku zatwierdzenia nieblokującego, zmniejszy to zagęszczenie w wątku głównym przez zmniejszenie obciążenia związanego z renderowaniem. Taki projekt nigdy nie byłby możliwy bez ulepszeń architektonicznych w narzędziu Composite After Paint.

A w planach jest więcej projektów (ciekawostka). Wreszcie mamy podstawę, która pozwala eksperymentować z redystrybucją pracy renderowania, i jesteśmy bardzo ciekawi, co da się osiągnąć.