Szczegółowa analiza renderowania: BlinkNG

Stefan Zager
Stefan Zager
Krzysiek Harrelson
Chris Harrelson

Blink odnosi się do implementacji platformy internetowej w Chromium i obejmuje wszystkie fazy renderowania poprzedzające komponowanie, którego kulminacją jest zatwierdzenie kompozytora. Więcej informacji o architekturze renderowania migawkowego znajdziesz w poprzednim artykule z tej serii.

Serwis Blink powstał w 1998 roku jako rozwidlenie platformy WebKit, która jest rozwidleniem platformy KHTML z 1998 roku. Zawiera on jeden z najstarszych (i najbardziej krytyczny) kodu w Chromium, a do 2014 roku zdecydowanie już był widoczny w przeglądarce. W tym roku rozpoczęliśmy szereg ambitnych projektów zwanych BlinkNG, których celem było rozwiązanie wieloletnich niedoskonałości w organizacji i strukturze kodu Blink. W tym artykule omawiamy projekt BlinkNG i jego projekty składowe: dlaczego stworzyliśmy ten projekt, co udało się osiągnąć, jakie są główne zasady, które ukształtowały ten projekt, a także jakie są możliwości wprowadzenia przyszłych ulepszeń.

Potok renderowania przed BlinkNG i po nim.

Renderowanie przed NG

Potok renderowania w Blink zawsze był koncepcyjnie podzielony na fazy (styl, układ, malowanie itd.), ale bariery abstrakcji były niewyraźne. Ogólnie rzecz biorąc, dane związane z renderowaniem składały się z długotrwałych, zmiennych obiektów. Obiekty te mogły być – i zostały – zmodyfikowane w dowolnym momencie oraz często poddawane recyklingowi i wykorzystywane przez kolejne aktualizacje renderowania. Nie można było rzetelnie odpowiedzieć na proste pytania, takie jak:

  • Czy trzeba zaktualizować dane wyjściowe związane ze stylem, układem lub wyrenderowaniem?
  • Kiedy te dane uzyskają „ostateczną” wartość?
  • Kiedy można zmodyfikować te dane?
  • Kiedy ten obiekt zostanie usunięty?

Jest wiele przykładów, m.in.:

Styl generowałby elementy ComputedStyle na podstawie arkuszy stylów, ale element ComputedStyle był niezmienny. W niektórych przypadkach mógł zostać zmodyfikowany na późniejszych etapach potoku.

Styl wygeneruje drzewo o wartości LayoutObject, a w polu Układ będzie można dodać adnotacje z informacjami o rozmiarze i położeniu. W niektórych przypadkach układ może nawet zmodyfikować strukturę drzewa. Nie było wyraźnej separacji danych wejściowych i wyjściowych w układzie.

Styl Styl generowałby dodatkowe struktury danych, które określały przebieg komponowania. Te struktury były modyfikowane w każdej fazie po zastosowaniu atrybutu style.

Na niższym poziomie typy danych renderowania składają się w dużej mierze z wyspecjalizowanych drzew (np. drzewo DOM, drzewo stylów, drzewo układu, drzewo właściwości farb), a fazy renderowania są implementowane jako rekurencyjne spacery po drzewach. Najlepiej, aby przechowywać drzewo: podczas przetwarzania danego węzła drzewa nie powinniśmy uzyskiwać dostępu do żadnych informacji poza drzewem podrzędnym, które wypływa na ten węzeł. To nigdy nie miało miejsca w rzeczywistości przed renderowaniem. Drzewa prowadzi często do informacji z elementów nadrzędnych przetwarzanych węzłów. Sprawiało to, że system był bardzo delikatny i podatny na błędy. Nie można było też rozpocząć chodzenia po drzewie z dowolnego miejsca poza jego korzeniem.

Na koniec w ramach potoku renderowania zastosowano wiele ram czasowych: wymuszone układy aktywowane przez JavaScript, częściowe aktualizacje wyzwalane podczas wczytywania dokumentu, wymuszone aktualizacje przygotowujące do kierowania na zdarzenia, zaplanowane aktualizacje żądane przez system wyświetlania oraz specjalistyczne interfejsy API dostępne tylko dla kodu testowego. W procesie renderowania pojawiło się nawet kilka ścieżek rekurencyjnych i rekurencyjnych (czyli przeskakiwanie na początek jednego etapu z środka innego). Każda z tych Ramp ma swoje idiosynchroniczne działanie, a w niektórych przypadkach dane wyjściowe renderowania będą zależały od sposobu, w jaki została wprowadzona aktualizacja renderowania.

Co zmieniliśmy

BlinkNG składa się z wielu podprojektów, dużych i małych, których wspólnym celem jest wyeliminowanie opisanych wcześniej deficytów architektonicznych. W tych projektach zastosowano kilka wspólnych zasad mających na celu przekształcenie potoku renderowania w rzeczywisty potok:

  • Jednolity punkt wejścia: zawsze należy wprowadzać potok na początku.
  • Etapy działania: każdy etap powinien mieć dobrze określone dane wejściowe i wyjściowe, a jego działanie powinno być funkcjonalne, czyli deterministyczne i powtarzalne, a wyniki powinny zależeć wyłącznie od zdefiniowanych danych wejściowych.
  • Stałe dane wejściowe: dane wejściowe dowolnego etapu powinny być w rzeczywistości stałe podczas działania sceny.
  • Stałe dane wyjściowe: po zakończeniu etapu dane wyjściowe powinny być stałe przez pozostałą część aktualizacji renderowania.
  • Spójność punktów kontrolnych: na końcu każdego etapu dane renderowane do tej pory powinny ukształtować się w sposób spójny.
  • Deduplikacja: obliczaj poszczególne elementy tylko raz.

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

Cykl życia dokumentu

Klasa DocumentLifecycle śledzi postęp w procesie renderowania. Pozwala nam to przeprowadzić podstawowe testy, które egzekwują wymienione wcześniej niezmienniki, takie jak:

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

W trakcie wdrażania BlinkNG systematycznie eliminowaliśmy ścieżki kodu, które naruszały te niezmienniki, i umieściliśmy w kodzie więcej twierdzeń, aby uniknąć ewentualnych błędów.

Jeśli kiedykolwiek zdarzyło Ci się złapać choinkę, patrząc na niskopoziomowy kod renderujący, zadaj sobie pytanie „Skąd się tu wzięłam?”. Jak już wspomnieliśmy, potok renderowania składa się z różnych punktów wejścia. Wcześniej obejmowały to ścieżki wywołań rekurencyjnych i powracających oraz miejsca, w których rozpoczęliśmy proces rejestracji na etapie pośrednim, a nie od początku. W trakcie szkolenia BlinkNG przeanalizowaliśmy te ścieżki połączeń i ustaliliśmy, że wszystkie można ograniczyć do 2 podstawowych scenariuszy:

  • Wszystkie dane renderowania wymagają aktualizacji, np. podczas generowania nowych pikseli do wyświetlania lub wykonywania testu działania na potrzeby kierowania na zdarzenia.
  • Potrzebujemy aktualnej wartości dla konkretnego zapytania, na którą można uzyskać odpowiedź bez aktualizowania wszystkich danych renderowania. Dotyczy to większości zapytań JavaScript, np. node.offsetTop.

Obecnie do potoku renderowania można wejść tylko w 2 punkty odpowiadające tym 2 scenariuszom. Ścieżki kodu abonenta zostały usunięte lub refaktoryzowane i nie można już wejść do potoku, zaczynając od etapu pośredniego. Dzięki temu wyeliminowano wiele tajemnic dotyczących czasu i sposobu przeprowadzania aktualizacji renderowania, dzięki czemu łatwiej było zrozumieć działanie systemu.

Styl, układ i wstępne malowanie potoku

Łącznie fazy renderowania przed renderowaniem odpowiadają za te działania:

  • Uruchamianie algorytmu stylu kaskadowego w celu obliczenia końcowych właściwości stylu dla węzłów DOM.
  • Generowanie drzewa układu reprezentującego hierarchię ramek dokumentu.
  • Określam informacje o rozmiarze i położeniu wszystkich pól.
  • Zaokrąglanie lub przyciąganie geometrii subpikseli do granic całych pikseli na potrzeby malowania.
  • Określanie właściwości skomponowanych warstw (przekształcenia afinatywnego, filtrów, nieprzezroczystości lub innych elementów, które mogą być akcelerowane za pomocą GPU).
  • Określa, które treści zmieniły się od poprzedniej fazy malowania i wymagają pomalowania lub przemalowania (unieważnienie).

Ta lista nie uległa zmianie, ale przed BlinkNG większość tych prac doraźnie została rozpowszechniona na wielu fazach renderowania z wieloma powielonymi funkcjami i wbudowanymi deficytami. Na przykład etap style zawsze odpowiadał przede wszystkim za obliczanie właściwości ostatecznego stylu węzłów, ale w pewnych przypadkach specjalnych nie określaliśmy ostatecznych wartości właściwości stylu przed zakończeniem fazy style. W procesie renderowania nie było żadnego formalnego ani możliwego do wyegzekwowania punktu, w którym można by z całą pewnością stwierdzić, że informacje o stylu są kompletne i stałe.

Innym dobrym przykładem problemów sprzed BlinkNG jest unieważnienie farby. Dotychczas proces unieważniania malowania był stosowany na wszystkich fazach renderowania, które kończyły się renderowaniem. Przy modyfikowaniu stylu lub kodu układu trudno było się dowiedzieć, jakie zmiany są potrzebne w celu wyrenderowania logiki unieważniania. Łatwo było też popełnić błąd, który powodował zbyt częste lub niedostateczne unieważnianie. Więcej informacji o szczegółach starego systemu unieważniania obrazów znajdziesz w artykule z serii LayoutNG.

Przyciąganie geometrii subpiksela do granic całych pikseli na potrzeby malowania to przykład sytuacji, w której mieliśmy wiele implementacji tej samej funkcji i wykonaliśmy wiele nadmiarowych zadań. System renderowania używa jednej ścieżki kodu, która przyciąga piksele, i całkowicie oddzielnej ścieżki kodu – do sytuacji, w której potrzebne były jednorazowe i w trakcie wykonywania jednorazowych obliczania współrzędnych nakładanych pikselami poza kodem renderowania. Nie trzeba dodawać, że każda implementacja zawierała błędy, a ich wyniki nie zawsze były zgodne. Ponieważ takie informacje nie były zapisywane w pamięci podręcznej, system czasami powtarzałby dokładnie te same obliczenia, co dodatkowo obniżało wydajność.

Oto kilka ważnych projektów, które pozwoliły wyeliminować deficyt architektoniczny faz renderowania przed malowaniem.

Zespół projektu: wytyczaj fazę stylu

Ten projekt zetknął się z dwoma głównymi niedociągnięciami na etapie stylu, co uniemożliwiło spójną pracę z nim:

Istnieją dwa główne dane wyjściowe fazy stylu: ComputedStyle, które zawierają wynik uruchomienia algorytmu kaskadowego CSS nad drzewem DOM oraz drzewo LayoutObjects, które określa kolejność operacji dla fazy układu. Ogólnie rzecz biorąc, algorytm kaskadowy powinien zostać uruchomiony wyłącznie przed wygenerowaniem drzewa układu. Wcześniej te 2 operacje były przeplatane. Firma Project Squad podzieliła te 2 fazy na odrębne fazy.

Wcześniej ComputedStyle nie zawsze otrzymywała ostateczną wartość podczas ponownego obliczania stylu. W kilku sytuacjach ComputedStyle była aktualizowana na późniejszym etapie potoku. Zespół Project Squad przeprowadził refaktoryzację tych ścieżek kodu, więc element ComputedStyle nigdy nie jest modyfikowany po fazie stylu.

UkładNG: potokowanie fazy układu

Ten projekt pomnikowy – jeden z elementów RenderingNG – stanowił całkowite przebudowanie fazy renderowania układu. Nie omówimy tu całego projektu BlinkNG, ale istnieje kilka istotnych aspektów:

  • Wcześniej w fazie układu utworzono drzewo LayoutObject utworzone na etapie stylu oraz dodawane do niego adnotacje z informacjami o rozmiarze i pozycji. Dzięki temu udało się w prosty sposób oddzielić dane wejściowe od danych wyjściowych. Wprowadzono drzewo fragmentów, które jest podstawowymi danymi wyjściowymi układu w trybie tylko do odczytu i służą za podstawowe dane wejściowe w kolejnych fazach renderowania.
  • Układ UkładNG wzbogacił układ o właściwość Containment: podczas obliczania rozmiaru i pozycji danego obiektu LayoutObject nie patrzymy już poza drzewo podrzędne, którego źródłem jest ten obiekt. Wszystkie informacje potrzebne do aktualizacji układu danego obiektu są obliczane wcześniej i przekazywane algorytmowi jako dane wejściowe tylko do odczytu.
  • Wcześniej w przypadkach skrajnych, w których algorytm układu nie był dokładnie funkcjonalny, jego wynik był zależny od najnowszej wcześniejszej aktualizacji układu. Zespół UkładNG wyeliminował te przypadki.

Faza wstępnego renderowania

Wcześniej nie było tu formalnej fazy renderowania przed malowaniem, a jedynie wstępna faza malowania. Faza przed renderowaniem wzrosła dzięki uznaniu, że jest kilka powiązanych funkcji, które można najlepiej wdrożyć jako systematyczne poruszanie się po drzewie układu po utworzeniu układu, a przede wszystkim:

  • Zgłaszanie unieważnień renderowania: prawidłowe unieważnianie malowania jest bardzo trudne w trakcie układu, gdy mamy niekompletne informacje. Dużo łatwiej jest przeprowadzić poprawną i wydajną pracę, jeśli podzielimy ją na 2 różne procesy: podczas stylu i układu treści można oznaczyć prostą flagą wartości logicznej jako „prawdopodobnie wymaga unieważnienia renderowania”. Podczas spaceru po drzewie sprawdzamy te flagi i w razie potrzeby wydajemy unieważnienia.
  • Generowanie drzew właściwości farb: proces opisany bardziej szczegółowo.
  • Obliczanie i rejestrowanie lokalizacji wyrenderowanych w postaci pikseli: zarejestrowane wyniki można wykorzystać na etapie malowania, a także dla dowolnego kodu, który ich potrzebuje, bez konieczności wykonywania zbędnych obliczeń.

Drzewa nieruchomości: jednolita geometria

Drzewa właściwości zostały wprowadzone na początku korzystania z RenderingNG, aby radzić sobie ze 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 „warstwowej” hierarchii do reprezentowania geometrycznej relacji skomponowanej treści, ale szybko się rozpadło, ponieważ takie funkcje jak pozycja:fixed (pozycja:stał) stały się oczywiste. W hierarchii warstw pojawiły się dodatkowe nielokalne wskaźniki wskazujące na element nadrzędny warstwy przewijanej lub element nadrzędny warstwy, przez co wcześniej bardzo trudno było zrozumieć kod.

Drzewa właściwości rozwiązały ten problem, przedstawiając aspekt przewijania i klipów niezależny od pozostałych efektów wizualnych. Umożliwiło to prawidłowe modelowanie rzeczywistej struktury wizualnej i przewijanej stron internetowych. Następnie musieliśmy wdrożyć do drzew właściwości algorytmy, takie jak przekształcenie przestrzeni ekranu skomponowanych warstw lub sprawdzenie, które warstwy się przewinęły, a które nie.

Szybko zauważyliśmy, że w wielu innych miejscach kodu znajdują się podobne pytania geometryczne. Pełną listę znajdziesz w poście na temat struktur danych. Niektóre z nich miały zduplikowane implementacje tego samego działania kodu kompozytora; wszystkie miały inny podzbiór błędów i żadne z nich nie miały prawidłowo wymodelowanej rzeczywistej struktury witryny. Rozwiązanie stało się jasne: zgromadziliśmy wszystkie algorytmy geometryczne w jednym miejscu i refaktoryzował cały kod, aby z niego korzystać.

Algorytmy te z kolei opierają się na drzewach właściwości, dlatego drzewa właściwości stanowią kluczową strukturę danych (używaną w całym potoku RenderingNG). Aby osiągnąć ten cel, jakim jest scentralizowany kod geometryczny, musieliśmy znacznie wcześniej wprowadzić koncepcję drzew właściwości w potoku – we wstępnym procesie malowania – i zmienić wszystkie interfejsy API, które wymagały od nich wstępnego renderowania, zanim mogły zostać wykonane.

Ta historia jest kolejnym aspektem wzorca refaktoryzacji BlinkNG: wskazuje kluczowe obliczenia, refaktoryzację, aby uniknąć ich duplikowania, oraz tworzenie dobrze zdefiniowanych etapów potoku, które tworzą struktury danych, które do nich wpływają. Drzewa właściwości są obliczane dokładnie w momencie, gdy dostępne są wszystkie niezbędne informacje. Dbamy o to, aby drzewa właściwości nie mogły się zmieniać podczas późniejszego procesu renderowania.

Kompozyt po malowaniu: farba i kompozycja rurociągowa

Warstwa to proces ustalania, która zawartość DOM trafia do własnej skomponowanej warstwy (która z kolei reprezentuje teksturę GPU). Przed RenderingNG nakładanie się odbywało się przed malowaniem, a nie po nim (zobacz tutaj bieżący potok – zwróć uwagę na zmianę kolejności). Najpierw określamy, w których częściach modelu DOM znajdują się poszczególne warstwy skomponowane, a następnie następuje rysowanie list wyświetlania dla tych tekstur. Decyzje te zależały oczywiście od czynników takich jak to, które elementy DOM są animowane lub przewijane, mają przekształcenia 3D oraz które elementy namalowane są na wierzchu.

Spowodowało to poważne problemy, ponieważ w kodzie kodu występowały zależności cykliczne, co stanowi duży problem w przypadku potoku renderowania. Zobaczmy na przykładzie, dlaczego tak jest. Załóżmy, że musimy invalidate renderowanie (co oznacza, że musimy ponownie narysować listę wyświetlania, a następnie zrastrować ją jeszcze raz). Konieczność unieważnienia może wynikać ze zmiany w DOM albo od zmiany stylu lub układu. Oczywiście chcielibyśmy unieważnić tylko te fragmenty, które rzeczywiście zostały zmienione. Konieczne było wtedy ustalenie, których skomponowanych warstw zostało dotkniętych problemem, a następnie unieważnienie części lub wszystkich list wyświetlania tych warstw.

Oznacza to, że unieważnienie treści było uzależnione od modelu DOM, stylu, układu i wcześniejszych decyzji dotyczących warstw (wcześniejsze: znaczenie w przypadku poprzednio renderowanej klatki). Obecna konfiguracja warstw również zależy od tych czynników. A ponieważ nie mieliśmy 2 kopii wszystkich danych dotyczących warstw, trudno było odróżnić przeszłość od przyszłych decyzji dotyczących warstw. Znaleźliśmy więc mnóstwo kodu z rozumowaniem kołowym. Czasami prowadziło to do nielogicznego lub nieprawidłowego kodu, a nawet do awarii lub problemów z bezpieczeństwem, o ile nie byliśmy zbyt ostrożni.

Aby rozwiązać ten problem, na wczesnym etapie przedstawiliśmy koncepcję obiektu DisableCompositingQueryAsserts. W większości przypadków próba odczytania wcześniejszych decyzji dotyczących warstw powodowała błąd asercji i awarię przeglądarki w trybie debugowania. Dzięki temu uniknęliśmy wprowadzenia nowych błędów. W każdym przypadku, w którym kod był słusznie potrzebny do odpytywania wcześniejszych decyzji dotyczących warstw, umieszczamy kod w kodzie, który jest dostępny, przydzielając obiekt DisableCompositingQueryAsserts.

Naszym celem było usunięcie wszystkich obiektów DisableCompositingQueryAssert witryn wywołujących, a następnie zadeklarowanie, że kod jest bezpieczny i prawidłowy. Okazało się jednak, że niektórych wywołań w zasadzie nie dało się usunąć, o ile nałożenie warstw miało miejsce przed renderowaniem. Udało nam się je usunąć dopiero niedawno. Była to pierwsza przyczyna powstania projektu Composite After Paint. Dowiedzieliśmy się, że nawet jeśli masz dobrze zdefiniowany etap potoku operacji, to jeśli znajduje się on w niewłaściwym miejscu w potoku, w końcu utkniesz.

Drugim powodem utworzenia projektu Composite After Paint był błąd funkcji Fundamental Compositing. Jednym ze sposobów zgłaszania tego błędu jest to, że elementy DOM nie są dobrym odzwierciedleniem efektywnego lub kompletnego schematu warstw w przypadku zawartości strony internetowej w formacie 1:1. A ponieważ komponowanie miało miejsce przed renderowaniem, mniej lub bardziej zależało od elementów DOM, a nie od wyświetlanych list czy drzew właściwości. Podobnie jak w przypadku wprowadzenia drzew właściwości, tak samo jak w przypadku drzew właściwości, rozwiązanie wychodzi bezpośrednio z instalacji, jeśli wytyczysz właściwy etap potoku, uruchomisz go we właściwym czasie i dostarczysz odpowiednie struktury danych. Podobnie jak w przypadku drzew właściwości, stanowiła świetną okazję do zagwarantowania, że po zakończeniu fazy malowania danych wyjściowych nie da się zmienić we wszystkich kolejnych fazach rurociągu.

Zalety

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

  • Znacznie większa niezawodność: to pytanie jest proste. Czystszy kod z dobrze zdefiniowanymi i zrozumiałymi interfejsami jest łatwiejszy do zrozumienia, pisania i testowania. Zwiększa to jego niezawodność. Dzięki temu kod jest bezpieczniejszy i stabilniejszy, a jednocześnie mniej awarii i błędów związanych z korzystaniem z aplikacji po jego braku.
  • Rozszerzony zasięg testów: w ramach usługi BlinkNG dodaliśmy do naszego pakietu wiele nowych testów. Obejmuje to testy jednostkowe zapewniające ukierunkowaną weryfikację elementów wewnętrznych, testy regresji uniemożliwiające ponowne wprowadzenie starych błędów, które naprawiliśmy (czyli tak wiele!), oraz wiele dodatków do publicznego, utrzymywanego zbiorczo pakietu do testów platformy internetowej, którego wszystkie przeglądarki używają do mierzenia zgodności ze standardami internetowymi.
  • łatwiejsze rozbudowanie: jeżeli system jest podzielony na jasne elementy, nie musisz szczegółowo analizować pozostałych elementów, aby wyrobić postępy w obecnym; Dzięki temu każdy może łatwiej dodać wartość do kodu renderującego, nie będąc wielkim ekspertem, a także lepiej rozumieć zachowanie całego systemu.
  • Wydajność: optymalizowanie algorytmów napisanych w kodzie spaghetti jest dość trudne, ale prawie niemożliwe jest osiągnięcie jeszcze większych celów, takich jak uniwersalne przewijanie w wątkach i animacje czy procesy i wątki na potrzeby izolacji witryny bez takiego potoku. Równoległość może znacznie zwiększyć skuteczność, ale jest też niezwykle skomplikowana.
  • Pozyskiwanie i ograniczanie: wprowadziliśmy kilka nowych funkcji BlinkNG, które pozwalają pracować z wykorzystaniem nowych, nowatorskich sposobów. A co, jeśli chcemy na przykład uruchamiać potok renderowania tylko do wygaśnięcia budżetu? A może pominąć renderowanie w przypadku drzew podrzędnych, które obecnie nie są istotne dla użytkownika? Umożliwia to właściwość CSS content-visibility. A co ze stylem komponentu zależy od jego układu? To zapytania kontenera.

Studium przypadku: zapytania dotyczące kontenerów

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

Zapytanie kontenera pozwala stylom, które mają zastosowanie do danego elementu, zależne od rozmiaru układu elementu nadrzędnego. Ponieważ rozmiar układu jest obliczany podczas układu, oznacza to, że musimy ponownie obliczyć styl po układzie; ale przed układem odbywa się ponowne obliczanie stylu. Ten paradoks oparte na kurczaku i jajku jest powodem, dla którego nie mogliśmy wdrożyć zapytań dotyczących kontenerów przed BlinkNG.

Jak rozwiązać ten problem? Czy nie jest to zależność wsteczna potoku, czyli ten sam problem, który rozwiązano projekty takie jak Composite After Paint? Co gorsza, co się stanie, jeśli nowe style zmienią rozmiar elementu nadrzędnego? Czy nie będzie to czasem zapętlać się w nieskończoność?

Z reguły zależność kołową można rozwiązać, stosując właściwość „include” CSS, która umożliwia renderowanie poza elementem bez renderowania w poddrzewie danego elementu. Oznacza to, że nowe style zastosowane przez kontener nie mają wpływu na jego rozmiar, ponieważ zapytania dotyczące kontenerów wymagają izolacji.

W rzeczywistości to jednak nie wystarczyło i koniecznie trzeba było wprowadzić słabszy typ izolacji niż tylko ograniczanie rozmiaru. Wynika to z faktu, że kontener zapytań dotyczących kontenerów ma możliwość zmiany rozmiaru tylko w jednym kierunku (zwykle jest to blok) zgodnie z wymiarami w tekście. W związku z tym dodaliśmy koncepcję zasłaniania w tekście. Jak jednak widać w bardzo długiej sekcji w tej sekcji, przez długi czas nie było w ogóle jasne, czy można było zablokować rozmiar elementu wbudowanego.

Czym innym jest opisywanie powstrzymania w języku abstrakcyjnych specyfikacji, a co innego poprawne wdrożenie tej zasady. Przypomnijmy, że jednym z celów BlinkNG było wprowadzenie zasady ochrony przed drzewami, która stanowi główną logikę renderowania – podczas przemierzania poddrzewa nie należy podawać żadnych informacji spoza tego poddrzewa. Jak się działo (cóż, nie był to przypadek), jest znacznie bardziej przejrzysty i łatwiejszy do zaimplementowania, o ile kod renderujący przestrzega zasad przechowywania.

Przyszłość: tworzenie wiadomości poza głównym wątkiem i nie tylko.

Potok renderowania widoczny tutaj jest w rzeczywistości trochę wyprzedzający bieżącą implementację RenderingNG. Nakładanie warstw pokazuje, że jest ono wyłączone z wątku głównego, podczas gdy obecnie jest ono w wątku głównym. To jednak tylko kwestia czasu, ponieważ kompozyt po wyrenderowaniu został wysłany, a warstwy są już po wyrenderowaniu.

Aby zrozumieć, dlaczego jest to ważne i dokąd jeszcze może zaprowadzić, należy spojrzeć na architekturę silnika renderowania z pewnej 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 uruchamianie skryptu), jak i znaczną część renderowania. W rezultacie wątek główny jest często przesycony pracą, a obciążenie wątków jest często wąskim gardłem w całej przeglądarce.

Dobra wiadomość jest taka, że nie musi tak być. Ten aspekt architektury Chromium sięga czasów KHTML, gdy dominującym modelem programowania było wykonanie w jednym wątku. W chwili, gdy procesory wielordzeniowe stały się powszechnie używane w urządzeniach konsumenckich, jednowątkowe założenia zostały już gruntownie wbudowane w Blink (dawniej WebKit). Od dawna chcieliśmy wprowadzić więcej wątków w mechanizmie renderowania, ale w starym systemie było to po prostu niemożliwe. Jednym z głównych zadań w przypadku renderowania na żywo jest wydobycie się z tej dziury i umożliwienie przeniesienia renderowania, częściowo 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. Zgłoszenie nieblokujące to pierwszy krok do zmiany modelu wątków mechanizmu renderowania. Zatwierdzenie kompozytora (lub po prostu commit) to etap synchronizacji między wątkiem głównym a wątkiem kompozytora. W trakcie zatwierdzania tworzymy kopie danych renderowania utworzone w wątku głównym, aby były one używane przez kod komponujący w dół uruchomiony w wątku kompozytora. Podczas tej 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, gdy wątek kompozytora go kopiuje.

Zatwierdzenie nieblokujące wyeliminuje konieczność zatrzymania wątku głównego i oczekiwania na zakończenie etapu zatwierdzania – wątek główny będzie kontynuował pracę, gdy zatwierdzenia będą wykonywane jednocześ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 obciążenie w wątku głównym i zwiększy wydajność. Obecnie (z marca 2022 r.) mamy działający prototyp zobowiązania nieblokującego i przygotowujemy szczegółową analizę jego wpływu na skuteczność.

Oczekiwanie w skrzydłach to komponowanie poza głównym wątkiem. Jego zadaniem jest dopasowanie mechanizmu renderowania do ilustracji przez przeniesienie warstwy z wątku głównego do wątku roboczego. Podobnie jak zatwierdzenie nieblokujące, zmniejszy to obciążenie w wątku głównym przez zmniejszenie obciążenia renderowania. Taki projekt nigdy nie byłby możliwy bez ulepszeń architektonicznych programu Composite After Paint.

A właśnie przygotowujemy więcej projektów! W końcu mamy podstawę, która umożliwia eksperymentowanie z rozdzielaniem zadań renderowania, i jesteśmy bardzo ciekawi tego, co jest możliwe.