Blink to implementacja platformy internetowej w Chromium. Obejmuje ona wszystkie etapy renderowania przed składaniem, łącznie z kompilacją kompozytora. Więcej informacji o architekturze renderowania blink znajdziesz w poprzednim artykule z tej serii.
Blink powstał jako odgałęzienie WebKit, który z kolei jest odgałęzią KHTML, którego początki sięgają 1998 r. Zawiera on jeden z najstarszych (i najważniejszych) fragmentów kodu w Chromium, który w 2014 r. zdecydowanie odzwierciedlał swój wiek. W tym roku rozpoczęliśmy realizację ambitnego projektu o nazwie BlinkNG, którego celem jest rozwiązanie długotrwałych problemów z organizacją i strukturą kodu Blink. W tym artykule omawiamy BlinkNG i jego składowe projekty: dlaczego je stworzyliśmy, jakie osiągnęliśmy dzięki nim wyniki, jakie zasady projektowania były dla nich kluczowe oraz jakie dają możliwości dalszego ulepszania.
Renderowanie przed NG
Pipeline renderowania w Blink był zawsze podzielony na etapy (style, layout, paint itd.), ale bariery abstrakcji były nieszczelne. Ogólnie rzecz biorąc, dane związane z renderowaniem składały się z długotrwałych, zmiennych obiektów. Te obiekty mogły być i były modyfikowane w dowolnym momencie oraz często były odzyskiwane i wykorzystywane ponownie w kolejnych aktualizacjach renderowania. Nie można było uzyskać wiarygodnych odpowiedzi na proste pytania, takie jak:
- Czy styl, układ lub kolorystyka wymagają aktualizacji?
- Kiedy te dane uzyskają „ostateczną” wartość?
- Kiedy można modyfikować te dane?
- Kiedy ten obiekt zostanie usunięty?
Oto kilka przykładów:
Style generowałby ComputedStyle
na podstawie arkuszy stylów, ale ComputedStyle
nie był niezmienny; w niektórych przypadkach mógł zostać zmodyfikowany przez późniejsze etapy potoku.
Style generowałby drzewo LayoutObject
, a layout dodawałby do tych obiektów adnotacje z informacjami o rozmiarze i pozycji. W niektórych przypadkach schemat może nawet zmodyfikować strukturę drzewa. Brak wyraźnego oddzielenia danych wejściowych i wyjściowych w layout.
Style generowałby dodatkowe struktury danych, które określały przebieg kompozycji. Te struktury danych były modyfikowane w ramach każdej fazy po style.
Na niższym poziomie typy danych do renderowania składają się głównie z specjalnych drzew (np. drzewa DOM, drzewa stylów, drzewa układu, drzewa właściwości malowania); fazy renderowania są natomiast implementowane jako rekurencyjne przeszukiwania drzewa. W idealnym przypadku przeszukiwanie drzewa powinno być ograniczone: podczas przetwarzania danego węzła drzewa nie powinniśmy mieć dostępu do żadnych informacji spoza poddrzewa z tym węzłem. Przed RenderingNG nigdy tak nie było. W przechodzeniu po drzewie często uzyskiwano informacje od przodków węzła, który był przetwarzany. To spowodowało, że system był bardzo delikatny i podatny na błędy. Nie można też rozpocząć spaceru po drzewie nigdzie indziej niż u korzenia.
W końcu w kodzie było wiele punktów wejścia do łańcucha renderowania: wymuszone układy wywoływane przez JavaScript, częściowe aktualizacje wywoływane podczas wczytywania dokumentu, wymuszone aktualizacje przygotowujące do kierowania zdarzeń, zaplanowane aktualizacje żądane przez system wyświetlania oraz wyspecjalizowane interfejsy API dostępne tylko dla kodu testowego. W przetwarzaniu były nawet ścieżki rekursywne i reentrantowe (czyli przeskakiwanie na początku jednego etapu z początku innego). Każda z tych ramp miała swoje specyficzne zachowanie, a w niektórych przypadkach renderowanie było zależne od sposobu wywołania aktualizacji.
Co się zmieniło
BlinkNG składa się z wielu projektów podrzędnych, dużych i małych, które mają wspólny cel polegający na wyeliminowaniu opisanych wcześniej braków w architekturze. Te projekty mają kilka wspólnych zasad, które mają na celu uczynić proces renderowania bardziej efektywnym:
- Jednolity punkt dostępu: zawsze powinniśmy wchodzić do potoku na początku.
- Etapy funkcjonalne: 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 wyjścia powinny zależeć tylko od zdefiniowanych danych wejściowych.
- Stały sygnał wejściowy: sygnał wejściowy na dowolnym etapie powinien być w większości przypadków stały.
- Niezmiennicze dane wyjściowe: po zakończeniu etapu jego dane wyjściowe powinny być niezmienne do końca aktualizacji renderowania.
- Spójność punktów kontrolnych: na końcu każdego etapu wygenerowane do tej pory dane renderowania powinny być spójne.
- Usuwanie duplikatów zadań: każdy element jest przetwarzany tylko raz.
Pełna lista podprojektów BlinkNG byłaby nudna w czytaniu, ale poniżej znajdziesz kilka szczególnie ważnych.
Cykl życia dokumentu
Klasa DocumentLifecycle śledzi postępy w przetwarzaniu. Umożliwia nam to przeprowadzanie podstawowych kontroli, które narzucają przestrzeganie wymienionych wcześniej niezmienników, takich jak:
- Jeśli modyfikujemy właściwość ComputedStyle, cykl życia dokumentu musi być
kInStyleRecalc
. - Jeśli stan DocumentLifecycle ma wartość
kStyleClean
lub nowszą, funkcjaNeedsStyleRecalc()
musi zwracać wartość false dla dowolnego załączonego węzła. - Podczas przechodzenia do etapu cyklu życia paint stan cyklu życia musi być
kPrePaintClean
.
Podczas wdrażania BlinkNG systematycznie eliminowaliśmy ścieżki kodu, które naruszały te niezmienniki, i dodaliśmy do kodu wiele dodatkowych stwierdzeń, aby zapewnić brak regresji.
Jeśli kiedykolwiek zajmowałeś(-aś) się kodowaniem na niskim poziomie, pewnie zadajesz sobie pytanie „Jak to się stało?”. Jak już wspomnieliśmy, istnieje wiele punktów wejścia do łańcucha renderowania. Wcześniej obejmowało to rekurencyjne i reentrantne ścieżki wywołań oraz miejsca, w których wchodziliśmy do potoku w fazie pośredniej, a nie od początku. W ramach projektu BlinkNG przeanalizowaliśmy te ścieżki wywołań i stwierdziliśmy, że wszystkie można sprowadzić do 2 podstawowych scenariuszy:
- Wszystkie dane renderowania muszą zostać zaktualizowane, np. podczas generowania nowych pikseli do wyświetlania lub przeprowadzania testu trafień w przypadku kierowania na zdarzenie.
- 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
.
Teraz są tylko 2 punkty wejścia do potoku renderowania, które odpowiadają tym 2 scenariuszom. Ścieżki kodu reentrant zostały usunięte lub przebudowane, a do ścieżki można wejść tylko od początku. Dzięki temu udało się rozwiać wiele tajemnic dotyczących tego, kiedy i jak dokładnie odbywa się renderowanie. Dzięki temu znacznie łatwiej jest analizować działanie systemu.
Przetwarzanie w ramach procesu: styl, układ i przedmalowanie
Fazy renderowania przed paintem odpowiadają za:
- Uruchamianie algorytmu kaskady stylów w celu obliczenia ostatecznych właściwości stylów dla węzłów DOM.
- Generowanie drzewa układu reprezentującego hierarchię pól dokumentu.
- określanie informacji o rozmiarze i pozycji wszystkich pól;
- Zaokrąglanie lub przycinanie geometrii poniżej piksela do granicy pełnego piksela na potrzeby malowania.
- określanie właściwości złożonych warstw (transformacja afiniczna, filtry, krycie lub cokolwiek innego, co można przyspieszyć za pomocą GPU);
- Określanie, które treści zmieniły się od poprzedniej fazy renderowania i muszą zostać ponownie wyrenderowane (unieważnienie renderowania).
Ta lista się nie zmieniła, ale przed BlinkNG większość tej pracy była wykonywana w sposób ad hoc, rozłożona na wiele faz renderowania, z wiele powielające się funkcje i wbudowane nieefektywności. Na przykład faza style była zawsze odpowiedzialna za obliczanie końcowych właściwości stylów dla węzłów, ale w kilku szczególnych przypadkach wartości końcowych właściwości stylów określaliśmy dopiero po zakończeniu fazy style. W procesie renderowania nie było formalnego ani egzekwowalnego punktu, w którym moglibyśmy z pewnością stwierdzić, że informacje o stylu są kompletne i niezmienialne.
Innym dobrym przykładem problemu występującego przed BlinkNG jest unieważnienie malowania. Wcześniej unieważnienie renderowania było rozproszone w ramach wszystkich faz renderowania prowadzących do renderowania. Podczas modyfikowania kodu stylu lub układu trudno było określić, jakie zmiany w logice nieważności malowania są potrzebne, i łatwo było popełnić błąd, który prowadził do niedostatecznej lub nadmiernej nieważności. Więcej informacji o szczegółach starego systemu unieważniania renderowania znajdziesz w artykule z tej serii poświęconym LayoutNG.
Przypinanie geometrii układu do granicy pełnego piksela w celu nakładania jest przykładem sytuacji, w której mieliśmy wiele implementacji tej samej funkcji i wykonaliśmy dużo zbędącej się nakładania. System paint używał jednej ścieżki kodu z pixelowym zaokrągleniem, a w przypadku jednorazowego, dynamicznego obliczenia współrzędnych z pixelowym zaokrągleniem poza kodem painta używano zupełnie innej ścieżki kodu. Oczywiście każda implementacja miała swoje błędy, a ich wyniki nie zawsze były takie same. Ponieważ te informacje nie były przechowywane w pamięci podręcznej, system czasem wykonywał dokładnie te same obliczenia wielokrotnie, co również pogarszało wydajność.
Oto kilka ważnych projektów, które wyeliminowały niedociągnięcia architektoniczne w etapach renderowania przed etapem malowania.
Projekt Squad: przetwarzanie fazy stylizacji
W tym projekcie rozwiązaliśmy 2 główne problemy, które uniemożliwiały płynne przetwarzanie danych w ramach fazy stylizacji:
Faza stylów ma 2 główne dane wyjściowe: ComputedStyle
, zawierające wynik uruchomienia algorytmu kaskadowego CSS w drzewie DOM, oraz drzewo LayoutObjects
, które określa kolejność operacji w fazie układu. Teoretycznie algorytm kaskadowy powinien być uruchamiany przed wygenerowaniem drzewa układu, ale wcześniej te 2 operacje były przeplatane. Project Squad podzielił te 2 elementy na oddzielne, kolejne fazy.
Wcześniej ComputedStyle
nie zawsze uzyskiwał swoją ostateczną wartość podczas ponownego obliczania stylu. Wystąpiło kilka sytuacji, w których ComputedStyle
był aktualizowany na późniejszym etapie przepływu danych. Zespół projektowy przeprowadził refaktoryzację tych ścieżek kodu, dzięki czemu ComputedStyle
nigdy nie jest modyfikowany po fazie stylizacji.
LayoutNG: przetwarzanie fazy układu w ramach potoku
Ten monumentalny projekt, jeden z filarów RenderingNG, był całkowitym przepisaniem fazy renderowania układu. Nie będziemy tutaj omawiać całego projektu, ale warto zwrócić uwagę na kilka jego aspektów:
- Wcześniej faza układu otrzymywała drzewo
LayoutObject
utworzone przez fazę stylu i dodawało do niego adnotacje z informacjami o rozmiarze i pozycji. W związku z tym nie było czystego oddzielenia danych wejściowych od danych wyjściowych. W LayoutNG wprowadzono drzewo fragmentów, które jest podstawowym wyjściem z tylko do odczytu i służy jako podstawowy element wejściowy w kolejnych fazach renderowania. - LayoutNG wprowadził do układu właściwość ograniczania: podczas obliczania rozmiaru i położenia danego elementu
LayoutObject
nie sprawdzamy już poza poddrzewem z korzenia tego obiektu. Wszystkie informacje potrzebne do zaktualizowania układu danego obiektu są obliczane z wyprzedzeniem i przekazywane algorytmowi jako dane wejściowe tylko do odczytu. - Wcześniej występowały przypadki, w których algorytm układu nie działał prawidłowo: jego wynik zależał od ostatniej aktualizacji układu. LayoutNG wyeliminował te przypadki.
Faza renderowania wstępnego
Wcześniej nie było formalnej fazy renderowania przed malowaniem, tylko mieszanka operacji po ułożeniu. Faza przed malowaniem powstała w wyniku uznania, że istnieje kilka powiązanych funkcji, które można najlepiej zaimplementować jako systematyczne przechodzenie po drzewie układu po jego ukończeniu. Najważniejsze z nich to:
- Wydawanie nieważności malowania: bardzo trudno jest prawidłowo anulować ważność malowania podczas układania, gdy mamy niepełne informacje. Jest to znacznie łatwiejsze i może być bardzo wydajne, jeśli podzielisz ten proces na 2 odrębne procesy: podczas stylizacji i układu treści można oznaczyć prostą flagą logiczną jako „być może wymagającą unieważnienia renderowania”. Podczas przeszukiwania drzewa przed malowaniem sprawdzamy te flagi i w razie potrzeby unieważniamy nieprawidłowe wyniki.
- Generowanie drzew właściwości obiektów w programie Paint: proces opisany bardziej szczegółowo w dalszej części tego artykułu.
- Obliczanie i zapisywanie lokalizacji punktów obrazu: zarejestrowane wyniki mogą być używane przez fazę paint, a także przez dowolny kod po stronie klienta, który ich potrzebuje, bez zbędnych obliczeń.
Struktury obiektów: spójna geometria
Drzewa właściwości zostały wprowadzone na wczesnym etapie rozwoju RenderingNG, aby poradzić sobie ze złożonością przewijania, które w internecie ma inną strukturę niż wszystkie inne rodzaje efektów wizualnych. Przed drzewami właściwości kompozytor w Chromium używał pojedynczej hierarchii „warstw”, aby reprezentować geometryczne relacje między składanymi treściami, ale szybko okazało się to niewystarczające, gdy ujawniła się pełna złożoność funkcji takich jak position:fixed. Hierarchia warstw zawierała dodatkowe wskaźniki nielokalne wskazujące „element nadrzędny przewijania” lub „element nadrzędny klipu” warstwy, przez co kod stał się bardzo trudny do zrozumienia.
Rozwiązaniem tego problemu było oddzielenie przewijania i przycinania treści od wszystkich innych efektów wizualnych. Umożliwiło to prawidłowe modelowanie prawdziwej struktury wizualnej i przewijania stron internetowych. Następnie musieliśmy zaimplementować algorytmy na podstawie drzew właściwości, np. transformację przestrzeni ekranu złożonych warstw lub określenie, które warstwy się przewijały, a które nie.
Wkrótce zauważyliśmy, że w kodzie jest wiele innych miejsc, w których pojawiały się podobne pytania dotyczące geometrii. (pełniejsza lista znajduje się w artykule Główne struktury danych). Niektóre z nich zawierały zduplikowane implementacje tego samego, co robił kod kompozytora. Wszystkie zawierały inny podzbiór błędów, a żadna z nich nie odzwierciedlała prawidłowo prawdziwej struktury witryny. Wtedy rozwiązanie stało się jasne: scentralizowanie wszystkich algorytmów geometrycznych w jednym miejscu i przeformułowanie całego kodu.
Wszystkie te algorytmy są zależne od drzew usług, dlatego drzewa usług są kluczową strukturą danych, czyli są używane w całym procesie RenderingNG. Aby osiągnąć cel polegający na scentralizowanym kodzie geometrii, musieliśmy wprowadzić koncepcję drzew właściwości znacznie wcześniej w pipeline’u – w etapie wstępnego renderowania – i zmienić wszystkie interfejsy API, które teraz od nich zależą, tak aby wymagały uruchomienia wstępnego renderowania przed wykonaniem.
Ten przykład to kolejny aspekt wzorca refaktoryzacji BlinkNG: zidentyfikuj kluczowe obliczenia, przeprowadź refaktoryzację, aby uniknąć ich duplikowania, i utwórz dobrze zdefiniowane etapy potoku, które tworzą struktury danych, które je zasilają. Obliczamy drzewa właściwości dokładnie w momencie, gdy dostępne są wszystkie niezbędne informacje. Sprawdzamy też, czy drzewa właściwości nie ulegają zmianie podczas wykonywania kolejnych etapów renderowania.
Kompilacja po malowaniu: przetwarzanie i kompilowanie w ramach procesu
Warstwowanie to proces określania, które treści DOM mają trafić do własnego złożonego poziomu (który z kolei reprezentuje teksturę GPU). Przed RenderingNG warstwowanie było wykonywane przed malowaniem, a nie po nim (patrz tutaj, aby poznać obecny przepływ danych – zwróć uwagę na zmianę kolejności). Najpierw określamy, które części DOM-u trafiły do której złożonej warstwy, a dopiero potem rysujemy listy wyświetlania dla tych tekstur. Oczywiście decyzje zależały od takich czynników, jak elementy DOM, które były animowane lub przewijane, lub miały przekształcenia 3D, oraz które elementy były nakładane na inne.
Spowodowało to poważne problemy, ponieważ wymagało to mniej lub bardziej tworzenia pętli zależności w kodzie, co jest dużym problemem w przypadku łańcucha renderowania. Zobaczmy, dlaczego tak jest. Załóżmy, że musimy unieważnić malowanie (co oznacza, że musimy ponownie narysować listę wyświetlania, a potem ponownie ją zarasteryzować). Konieczność unieważnienia może wynikać ze zmiany w DOM lub ze zmiany stylu lub układu. Oczywiście chcemy unieważnić tylko te części, które się zmieniły. Oznaczało to ustalenie, które złożone warstwy zostały dotknięte problemem, a następnie unieważnienie części lub wszystkich list wyświetlania dla tych warstw.
Oznacza to, że unieważnienie zależy od DOM, stylu, układu i wcześniejszych decyzji dotyczących warstw (wcześniejszych, czyli dotyczących poprzedniego renderowanego klatki). Ale obecna warstwa zależy też od wszystkich tych czynników. Ponieważ nie mieliśmy 2 kopii wszystkich danych warstw, trudno było odróżnić decyzje dotyczące warstw z przeszłości od tych z przyszłości. W efekcie mieliśmy dużo kodu, który zawierał pętlę. Czasami prowadziło to do nielogicznych lub nieprawidłowych kodów, a nawet do awarii lub problemów z bezpieczeństwem, jeśli nie byliśmy zbyt ostrożni.
Aby rozwiązać ten problem, wprowadziliśmy na wczesnym etapie koncepcję obiektu DisableCompositingQueryAsserts
. W większości przypadków, gdy kod próbował wysłać zapytanie o poprzednie decyzje dotyczące warstw, powodowało to błąd asercji i zawieszanie się 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 potrzebuje zapytań o poprzednie decyzje dotyczące warstw, dodajemy kod, który umożliwia to poprzez przydzielenie obiektu DisableCompositingQueryAsserts
.
Planowaliśmy z czasem pozbyć się wszystkich obiektów call sites DisableCompositingQueryAssert
, a potem zadeklarować, że kod jest bezpieczny i prawidłowy. Okazało się jednak, że usunięcie niektórych wywołań było niemożliwe, ponieważ warstwowanie nastąpiło przed malowaniem. (udało nam się to zrobić dopiero niedawno). Był to pierwszy powód, który udało się znaleźć w przypadku projektu „Composite After Paint”. Okazało się, że nawet jeśli masz dobrze zdefiniowaną fazę procesu dla danej operacji, ale jest ona umieszczona w niewłaściwym miejscu, ostatecznie dojdzie do jej zawieszenia.
Drugim powodem projektu „Composite After Paint” był błąd w podstawowym modułu do tworzenia kompozycji. Jednym z możliwych sposobów opisania tego błędu jest stwierdzenie, że elementy DOM nie odzwierciedlają w pełni i w prosty sposób schematu warstw treści strony internetowej. A ponieważ kompozycja była wykonywana przed malowaniem, była mniej lub bardziej zależna od elementów DOM, a nie od list wyświetlania ani drzew właściwości. Jest to bardzo podobny powód do wprowadzenia drzew usług. Podobnie jak w przypadku drzew usług, rozwiązanie jest oczywiste, jeśli określisz odpowiednią fazę potoku, uruchomisz go we właściwym czasie i zapewnisz mu odpowiednie kluczowe struktury danych. Podobnie jak w przypadku drzew usług, była to dobra okazja, aby zagwarantować, że po zakończeniu fazy malowania jej dane wyjściowe nie ulegną zmianie na kolejnych etapach procesu.
Zalety
Jak już wiesz, dobrze zdefiniowany proces renderowania przynosi ogromne korzyści długoterminowe. Jest ich więcej, niż myślisz:
- Zwiększona niezawodność: ta kwestia jest dość oczywista. Lepiej zrozumiały kod z dobrze zdefiniowanymi i zrozumiałymi interfejsami jest łatwiejszy do zrozumienia, napisania i przetestowania. Dzięki temu jest ona bardziej niezawodna. Dzięki temu kod jest bezpieczniejszy i bardziej stabilny, a w rezultacie rzadziej ulega awariom i zawiera błędy związane z możliwością korzystania z usługi bez uiszczenia opłaty.
- Rozszerzony zakres testów: w ramach BlinkNG dodaliśmy do naszego pakietu wiele nowych testów. Obejmuje to testy jednostkowe, które zapewniają ukierunkowaną weryfikację wewnętrznych elementów; testy regresji, które zapobiegają ponownemu wprowadzaniu starych błędów, które zostały już naprawione (tak wiele!); oraz wiele dodatków do publicznego, współdzielonego zestawu testów platformy internetowej, którego używają wszystkie przeglądarki do pomiaru zgodności ze standardami internetowymi.
- Łatwiejsza rozbudowalność: jeśli system jest podzielony na wyraźne komponenty, nie trzeba rozumieć innych komponentów na żadnym poziomie szczegółowości, aby móc rozwijać obecny. Dzięki temu każdy może łatwiej wzbogacać kod renderowania bez konieczności bycia ekspertem. Ułatwia to też analizowanie działania całego systemu.
- Skuteczność: optymalizacja algorytmów napisanych w kodzie spaghetti jest trudna, ale bez takiego potoku jest prawie niemożliwe, aby osiągnąć jeszcze większe cele, takie jak uniwersalne przewijanie i animacja w postaci wątków czy procesy i wątki służące do izolacji witryny. Parallizm może znacznie zwiększyć wydajność, ale jest też bardzo skomplikowany.
- Zysk i ograniczenie: dzięki BlinkNG udostępniliśmy kilka nowych funkcji, które wykorzystują potok w nowe i innowacyjne sposoby. Co zrobić, jeśli chcemy uruchomić ścieżkę renderowania tylko do czasu wygaśnięcia budżetu? Czy można pominąć renderowanie poddrzew, które nie są obecnie istotne dla użytkownika? Właśnie to umożliwia właściwość CSS content-visibility. A co z zależnością stylu komponentu od jego układu? To zapytania dotyczące kontenera.
Studium przypadku: zapytania dotyczące kontenera
Zapytania kontenerowe to bardzo oczekiwana funkcja platformy internetowej (od lat była najczęściej zgłaszaną przez programistów CSS). Jeśli jest tak świetny, to dlaczego jeszcze nie istnieje? Wynika to z tego, że implementacja zapytań kontenera wymaga bardzo dokładnego zrozumienia i kontroli relacji między kodem stylu a kodem układu. Przyjrzyjmy się temu bliżej.
Zapytanie dotyczące kontenera umożliwia, aby style stosowane do elementu zależały od rozmiaru układu przodka. Rozmiar układu jest obliczany podczas układania, co oznacza, że musimy przeprowadzić ponowny obliczanie stylu po ułożeniu, ale przeliczenie stylu jest wykonywane przed układaniem. Ten paradoks „jajko czy kura” to jedyny powód, dla którego nie mogliśmy wdrożyć zapytań dotyczących kontenerów przed BlinkNG.
Jak możemy rozwiązać ten problem? Czy nie jest to zależność od starszych wersji pipeline, czyli ten sam problem, który rozwiązały projekty takie jak Composite After Paint? Co gorsza, co się stanie, jeśli nowe style zmienią rozmiar przodka? Czy to nie spowoduje czasami nieskończonego pętli?
W zasadzie można rozwiązać ten problem, używając właściwości CSS contain, która pozwala na renderowanie poza elementem niezależne od renderowania w poddrzewiu tego elementu. Oznacza to, że nowe style zastosowane przez kontener nie mogą wpływać na jego rozmiar, ponieważ zapytania kontenera wymagają ograniczeń.
Okazało się jednak, że to nie wystarczyło, i trzeba było wprowadzić słabszy typ ograniczeń niż tylko ograniczenie rozmiaru. Dzieje się tak, ponieważ często chcemy, aby kontener zapytań kontenera mógł zmieniać rozmiar tylko w jednym kierunku (zwykle bloku) na podstawie wymiarów w tekście. Dlatego dodano koncepcję zawierania rozmiaru w tekście. Jak jednak widać z bardzo długiej notatki w tej sekcji, przez długi czas nie było jasne, czy wstawienie rozmiaru w ramce jest możliwe.
Opisujesz kontener w abstrakcyjnych specyfikacjach, ale jego prawidłowe wdrożenie to już zupełnie inna sprawa. Przypominamy, że jednym z celów BlinkNG było wprowadzenie zasady ograniczania do drzewa w ramach głównej logiki renderowania: podczas przeszukiwania poddrzewa nie powinno być wymagane żadne źródło informacji spoza tego poddrzewa. Jak się okazuje (nie było to do końca przypadkowe), znacznie łatwiej jest wdrożyć ograniczenie CSS, jeśli kod renderowania przestrzega zasady ograniczania.
Przyszłość: kompozycja poza wątkiem głównym... i nie tylko
Proces renderowania pokazany tutaj jest nieco bardziej zaawansowany niż obecna implementacja RenderingNG. Wątek pokazuje, że podział na warstwy jest wyłączony w wątku głównym, podczas gdy obecnie jest on nadal włączony. Jest to jednak tylko kwestia czasu, ponieważ kompozycja po malowaniu została już wdrożona, a warstwowanie następuje po malowaniu.
Aby zrozumieć, dlaczego jest to ważne i do czego jeszcze może prowadzić, musimy przyjrzeć się architekturze silnika renderowania z trochę szerszej perspektywy. Jednym z największych problemów utrudniających poprawę wydajności Chromium jest fakt, że główny wątek procesora graficznego obsługuje zarówno główną logikę aplikacji (czyli uruchamianie skryptu), jak i większą część renderowania. W rezultacie wątek główny jest często obciążony pracą, a zatrzymanie wątku głównego często jest wąskim gardłem w całym przeglądarce.
Dobra wiadomość jest taka, że nie musi tak być. Ten aspekt architektury Chromium sięga czasów KHTML, gdy jednowątkowe wykonywanie było dominującym modelem programowania. Gdy procesory wielordzeniowe stały się powszechne na urządzeniach konsumenckich, założenie o jednowątkowym działaniu było już wbudowane w Blink (wcześniej WebKit). Od dawna chcieliśmy wprowadzić do silnika renderowania więcej wątków, ale w starym systemie było to po prostu niemożliwe. Jednym z głównych celów Rendering NG było wydostanie się z tego bagna i umożliwienie przeniesienia części lub całości pracy związanej z renderowaniem do innego wątku (lub wątków).
Teraz, gdy BlinkNG zbliża się do ukończenia, zaczynamy już eksplorować tę dziedzinę. Nieblokujący commit to pierwszy krok w kierunku zmiany modelu wątków w renderze. Komitowanie kompozytora (lub po prostu komitowanie) to krok synchronizacji między wątkiem głównym a wątkiem kompozytora. Podczas zatwierdzania tworzymy kopie danych renderowania, które są generowane w wątku głównym, aby były używane przez kod do skompilowania na wyjściu, który działa w wątku kompozytora. Podczas synchronizacji wykonywanie wątku głównego jest wstrzymane, a kod kopiowania jest uruchamiany na wątku kompozytora. Ma to na celu zapewnienie, że wątek główny nie modyfikuje danych renderowania, gdy wątek kompozytora je kopiuje.
Dzięki nieblokującemu zatwierdzaniu nie trzeba będzie zatrzymywać wątku głównego i czekać na zakończenie etapu zatwierdzania. Wątek główny będzie nadal wykonywać swoje zadania, a zatwierdzenie będzie wykonywane równolegle w wątku kompozytora. Ogólny efekt zastosowania funkcji Non-Blocking Commit to skrócenie czasu poświęcanego na renderowanie w wątku głównym, co zmniejszy obciążenie tego wątku i poprawi wydajność. W momencie pisania tego tekstu (marzec 2022 r.) mamy działający prototyp bezblokującego zatwierdzenia i przygotowujemy się do przeprowadzenia szczegółowej analizy jego wpływu na wydajność.
W przygotowaniu jest renderowanie poza głównym wątkiem, którego celem jest dostosowanie silnika renderowania do ilustracji przez przeniesienie warstwowania z głównego wątku do wątku pomocniczego. Podobnie jak w przypadku zatwierdzania bez blokowania, spowoduje to zmniejszenie natężenia w wątku głównym przez zmniejszenie obciążenia renderowaniem. Taki projekt nie byłby możliwy bez ulepszeń architektury Composite After Paint.
Mamy też w planach więcej projektów (nie ma tu żadnego żartu). W końcu mamy podstawę, która umożliwia eksperymentowanie z redistributing rendering work. Nie możemy się doczekać, aż zobaczymy, co jest możliwe.