RenderingNG im Detail: BlinkNG

Stefan Zager
Stefan Zager
[Vorname 1]
Chris Harrelson

Blink bezieht sich auf die Implementierung der Webplattform in Chromium. Sie umfasst alle Phasen des Renderings, die vor dem Compositing stattfinden, und endet im Compositor-Commit. Weitere Informationen zur Architektur des Blink-Renderings finden Sie in einem vorherigen Artikel dieser Reihe.

Blink begann als eine Abspaltung von WebKit, das wiederum eine Abspaltung von KHTML aus dem Jahr 1998 ist. Sie enthält einige der ältesten (und wichtigsten) Code-Elemente in Chromium und wurde 2014 auch schon längst in die Jahre gekommen. In diesem Jahr starteten wir unter dem Motto BlinkNG eine Reihe ehrgeiziger Projekte mit dem Ziel, langjährige Mängel in der Organisation und der Struktur des Blink-Codes zu beheben. In diesem Artikel geht es um BlinkNG und die zugehörigen Projekte: Warum wir sie gemacht haben, was sie erreicht haben, welche Leitprinzipien das Design zugrunde gelegt hat und welche Möglichkeiten es sich für zukünftige Verbesserungen bietet.

Die Rendering-Pipeline vor und nach BlinkNG.

Rendering vor NG

Die Rendering-Pipeline in Blink war immer konzeptionell in Phasen unterteilt (Stil, Layout, Farbe usw.), aber die Abstraktionsbarrieren waren undeicht. Im Großen und Ganzen bestanden die mit dem Rendering verbundenen Daten aus langlebigen, änderbaren Objekten. Diese Objekte konnten jederzeit umgestaltet werden – und wurden auch später noch geändert. Sie wurden häufig recycelt und durch aufeinanderfolgende Rendering-Updates wiederverwendet. Es war unmöglich, zuverlässig einfache Fragen wie die folgenden zu beantworten:

  • Muss die Ausgabe von Stil, Layout oder Farbe aktualisiert werden?
  • Wann erhalten diese Daten ihren „endgültigen“ Wert?
  • Wann dürfen diese Daten geändert werden?
  • Wann wird dieses Objekt gelöscht?

Hier einige Beispiele:

Der Stil würde ComputedStyles auf der Grundlage von Stylesheets generieren, aber ComputedStyle war nicht unveränderlich. In einigen Fällen wurde er in späteren Pipelinephasen geändert.

Style würde eine Baumstruktur von LayoutObject generieren, woraufhin layout diese Objekte mit Größen- und Positionsinformationen annotiert. In einigen Fällen würde durch layout sogar die Baumstruktur verändert. In layout gab es keine klare Trennung zwischen den Ein- und Ausgaben.

Style erzeugte Zubehör-Datenstrukturen, die den Verlauf der Compositing-Funktion vorgeben, und diese Datenstrukturen wurden in jeder Phase nach dem Style geändert.

Auf einer niedrigeren Ebene bestehen Rendering-Datentypen größtenteils aus spezialisierten Baumstrukturen (z. B. DOM-Baum, Stilbaum, Layoutbaum, Paint-Eigenschaftenbaum). Rendering-Phasen werden als rekursive Baumwanderungen implementiert. Idealerweise sollte eine Baumtour eingeschlossen sein: Bei der Verarbeitung eines Baumknotens sollten wir nicht auf Informationen außerhalb der Unterstruktur zugreifen, die an diesem Knoten verankert ist. Das war vor dem RenderingNG nie der Fall. Der Baum geht häufig auf Informationen von den Ancestors des zu verarbeitenden Knotens durch. Dies machte das System sehr instabil und fehleranfällig. Außerdem war es unmöglich, von irgendwo außer der Wurzel des Baumes einen Spaziergang zu machen.

Schließlich gab es viele Übergänge in die Rendering-Pipeline, die über den gesamten Code verteilt waren: erzwungene Layouts, die durch JavaScript ausgelöst wurden, Teilaktualisierungen, die beim Laden des Dokuments ausgelöst wurden, erzwungene Aktualisierungen zur Vorbereitung für das Ereignis-Targeting, geplante Aktualisierungen, die vom Anzeigesystem angefordert wurden, und spezialisierte APIs, die nur für Testcode zur Verfügung standen, um nur einige zu nennen. Es gab sogar einige rekursiv und wiederkehrende Pfade in der Rendering-Pipeline, d. h., es wurde von der Mitte einer anderen an den Anfang einer Phase gesprungen. Jede dieser Verbindungsrampen zeigte ein eigenes eigenständiges Verhalten. In einigen Fällen hing die Ausgabe des Renderings davon ab, wie die Rendering-Aktualisierung ausgelöst wurde.

Was wir geändert haben

BlinkNG besteht aus vielen großen und kleinen Unterprojekten mit dem gemeinsamen Ziel, die zuvor beschriebenen Architekturdefizite zu beseitigen. Für diese Projekte gelten einige Leitprinzipien, die darauf abzielen, die Rendering-Pipeline einer echten Pipeline zu verleihen:

  • Einheitlicher Einstiegspunkt: Die Pipeline sollte immer am Anfang stehen.
  • Funktionale Phasen: Jede Phase sollte klar definierte Ein- und Ausgaben haben. Ihr Verhalten sollte funktional, d. h. deterministisch und wiederholbar, sein und die Ausgaben sollten nur von den definierten Eingaben abhängen.
  • Konstante Eingaben: Die Eingaben einer Phase sollten effektiv konstant sein, während die Phase läuft.
  • Unveränderliche Ausgaben: Sobald eine Phase abgeschlossen ist, sollten ihre Ausgaben für den Rest der Rendering-Aktualisierung unveränderlich sein.
  • Checkpoint-Konsistenz: Am Ende jeder Phase sollten die bisher erzeugten Renderingdaten einen inkonsistenten Status haben.
  • Deduplizierung von Arbeit: Jedes Element wird nur einmal berechnet.

Eine vollständige Liste der BlinkNG-Unterprojekte wäre zwar mühsam, aber folgende Konsequenzen sind besonders wichtig.

Der Dokumentlebenszyklus

Die Klasse DocumentLifecycle verfolgt unseren Fortschritt in der Renderingpipeline. Damit können grundlegende Prüfungen durchgeführt werden, bei denen die zuvor aufgeführten Invarianten erzwungen werden, z. B.:

  • Wenn wir eine ComputedStyle-Eigenschaft ändern, muss der Dokumentlebenszyklus kInStyleRecalc sein.
  • Wenn der DocumentLifecycle-Status kStyleClean oder höher ist, muss NeedsStyleRecalc() für alle angehängten Knoten false zurückgeben.
  • Wenn Sie in die Lebenszyklusphase paint beginnen, muss der Lebenszyklusstatus kPrePaintClean sein.

Bei der Implementierung von BlinkNG haben wir systematisch Codepfade beseitigt, die gegen diese Invarianten verstoßen, und viele weitere Assertions im Code verteilt, um sicherzustellen, dass kein Regress erfolgt.

Wenn Sie sich jemals Low-Level-Rendering-Code angesehen haben, fragen Sie sich vielleicht: „Wie bin ich hier gekommen?“ Wie bereits erwähnt, gibt es eine Vielzahl von Einstiegspunkten in die Rendering-Pipeline. Früher waren das rekursive Anrufpfade und Pfade mit wiederkehrenden Teilnehmern sowie Stellen, an denen wir die Pipeline in einer Zwischenphase befüllt haben, anstatt bei null anzufangen. Im Verlauf von BlinkNG haben wir diese Anrufpfade analysiert und festgestellt, dass sich alle auf zwei grundlegende Szenarien reduzieren lassen:

  • Alle Renderingdaten müssen aktualisiert werden, z. B. wenn neue Pixel für Displayanzeigen generiert oder ein Treffertest für das Ereignis-Targeting durchgeführt wird.
  • Es wird ein aktueller Wert für eine bestimmte Abfrage benötigt, der beantwortet werden kann, ohne alle Rendering-Daten zu aktualisieren. Dazu gehören die meisten JavaScript-Abfragen, z. B. node.offsetTop.

Es gibt jetzt nur noch zwei Einstiegspunkte in die Rendering-Pipeline, entsprechend diesen beiden Szenarien. Die Codepfade für neu eintretende Programme wurden entfernt oder refaktoriert und es ist nicht mehr möglich, in einer Zwischenphase in die Pipeline zu gelangen. Dadurch sind viele Rätsel rund um den genauen Zeitpunkt und die Art von Rendering-Aktualisierungen eliminiert, was das Verhalten des Systems viel leichter nachvollziehen kann.

Stil, Layout und Voranstriche mit Rohrleitungen

Insgesamt sind die Renderingphasen vor paint für Folgendes verantwortlich:

  • Stil-Kaskaden-Algorithmus ausführen, um endgültige Stileigenschaften für DOM-Knoten zu berechnen
  • Die Layoutstruktur generiert, die die Feldhierarchie des Dokuments darstellt.
  • Es werden Informationen zu Größe und Position für alle Felder ermittelt.
  • Rundung oder Andocken der Subpixel-Geometrie auf ganze Pixelgrenzen für das Painting.
  • Bestimmen der Eigenschaften zusammengesetzter Ebenen (affine Transformation, Filter, Deckkraft oder alles andere, was GPU-beschleunigt werden kann).
  • Ermitteln, welche Inhalte sich seit der vorherigen Malphase geändert haben und neu gestrichen oder neu gestrichen werden müssen (Farbentfernung)

Diese Liste hat sich nicht geändert, aber vor BlinkNG wurde ein Großteil dieser Arbeit auf Ad-hoc-Weise über mehrere Rendering-Phasen mit vielen duplizierten Funktionen und integrierten Ineffizienzen verteilt. Die Stil-Phase war beispielsweise schon immer für die Berechnung der endgültigen Stileigenschaften für Knoten verantwortlich. Es gab jedoch einige Sonderfälle, bei denen wir die endgültigen Stileigenschaftswerte erst nach Abschluss der Phase Stil festgelegt haben. Im Renderingprozess gab es keinen formalen oder durchsetzbaren Punkt, an dem wir mit Sicherheit sagen konnten, dass die Stilinformationen vollständig und unveränderlich sind.

Ein weiteres gutes Beispiel für Probleme vor BlinkNG ist die Farbentwertung. Früher wurde die Farbentwertung in allen Rendering-Phasen bis zur Painting-Phase verstreut. Beim Ändern des Stils oder des Layoutcodes war es schwierig, zu wissen, welche Änderungen an der Entwertungslogik erforderlich waren, und es war leicht, einen Fehler zu machen, der zu Fehlern mit unzureichender oder übermäßiger Entwertung führte. Weitere Informationen zu den Feinheiten des alten Systems zur Entwertung von Farben finden Sie im Artikel LayoutNG.

Das Andocken der Subpixel-Layoutgeometrie an ganze Pixelgrenzen zum Malen ist ein Beispiel dafür, dass wir mehrere Implementierungen derselben Funktionalität und viel redundante Arbeit hatten. Das Farbsystem hat einen Codepfad zum Einsetzen von Pixeln und einen völlig separaten Codepfad verwendet, der immer dann verwendet wurde, wenn wir eine einmalige, spontane Berechnung von an Pixel gesetzten Koordinaten außerhalb des Farbcodes benötigen. Natürlich kam es bei jeder Implementierung zu Fehlern und die Ergebnisse entsprachen nicht immer. Da diese Informationen nicht im Cache gespeichert wurden, führte das System manchmal genau dieselbe Berechnung wiederholt durch, was die Leistung zusätzlich belastet.

Hier sind einige bedeutende Projekte aufgeführt, bei denen die architektonischen Probleme der Rendering-Phasen vor dem Streichen beseitigt wurden.

Project Squad: Die Stilphase optimieren

Bei diesem Projekt wurden in der Stilphase zwei Hauptdefizite behoben, die eine ordnungsgemäße Pipeline verhinderten:

Es gibt zwei primäre Ausgaben der Stilphase: ComputedStyle enthält das Ergebnis der Ausführung des CSS-Kaskadenalgorithmus über der DOM-Baumstruktur und einen Baum von LayoutObjects, der die Reihenfolge der Vorgänge für die Layoutphase festlegt. Das Ausführen des Kaskadenalgorithmus sollte streng vor dem Generieren des Layoutbaums erfolgen. Bisher waren diese beiden Vorgänge verschränkt. Project Squad gelang es, diese beiden in separate, aufeinanderfolgende Phasen aufzuteilen.

Bisher hat ComputedStyle bei der Stilneuberechnung nicht immer den endgültigen Wert erhalten. Es gab einige Situationen, in denen ComputedStyle in einer späteren Pipelinephase aktualisiert wurde. Project Squad hat diese Codepfade refaktoriert, sodass ComputedStyle nach der Stilphase nie geändert wird.

LayoutNG: Die Layout-Phase per Pipe einschränken

Bei diesem Monumentalprojekt – einem der Eckpfeiler von RenderingNG – wurde die Phase des Layout-Renderings komplett neu geschrieben. Wir werden hier nicht dem gesamten Projekt gerecht werden, aber es gibt einige bemerkenswerte Aspekte für das gesamte BlinkNG-Projekt:

  • Zuvor erhielt die Layoutphase eine Baumstruktur von LayoutObject, die in der Stilphase erstellt wurde, und die Baumstruktur mit Informationen zur Größe und Position versehen. Daher konnten Eingaben und Ausgaben nicht sauber getrennt werden. LayoutNG hat den Fragmentbaum eingeführt. Dabei handelt es sich um die primäre schreibgeschützte Ausgabe des Layouts, die als primäre Eingabe für nachfolgende Renderingphasen dient.
  • LayoutNG hat die containment-Eigenschaft in das Layout integriert: Bei der Berechnung der Größe und Position einer bestimmten LayoutObject wird nicht mehr außerhalb der Unterstruktur dieses Objekts gesucht. Alle Informationen, die zum Aktualisieren des Layouts für ein bestimmtes Objekt erforderlich sind, werden vorab berechnet und dem Algorithmus als schreibgeschützte Eingabe bereitgestellt.
  • Zuvor gab es Grenzfälle, in denen der Layout-Algorithmus nicht ganz funktionsfähig war: Das Ergebnis des Algorithmus hing von der letzten Layout-Aktualisierung ab. LayoutNG hat diese Fälle beseitigt.

Die Phase der Farbvorbereitung

Früher gab es keine formelle Rendering-Phase vor dem Malen, sondern lediglich eine Menge an Vorgängen nach dem Layout. Die Phase Vormalen entstand aus der Erkenntnis, dass es einige verwandte Funktionen gab, die sich am besten als systematischen Durchlauf des Layoutbaums nach Fertigstellung des Layouts implementieren lassen. Das Wichtigste ist:

  • Angaben entwerten lassen: Wenn uns unvollständige Informationen vorliegen, ist es sehr schwierig, Farbentwertungen während des Layouts richtig vorzunehmen. Es ist viel einfacher, richtig zu arbeiten, und kann auch sehr effizient sein, wenn er in zwei verschiedene Prozesse unterteilt wird: Während des Stils und des Layouts können Inhalte mit einem einfachen booleschen Flag als „Muss malen entwertet werden“ markiert werden. Während des Durchstreichens des Baums überprüfen wir diese Markierungen und führen bei Bedarf Entwertungen durch.
  • Erstellen von Paint-Eigenschaftenbäumen: Ein Prozess, der weiter unten ausführlicher beschrieben wird.
  • An Pixel angedockte Positionen berechnen und erfassen: Die aufgezeichneten Ergebnisse können in der Paint-Phase und auch von jedem nachgelagerten Code verwendet werden, der sie ohne redundante Berechnungen benötigt.

Immobilienbäume: Einheitliche Geometrie

Property-Bäume wurden früh in RenderingNG eingeführt, um dem komplexen Scrollen Rechnung zu tragen, das im Web eine andere Struktur als alle anderen visuellen Effekte aufweist. Vor der Erstellung von Property-Bäumen verwendete der Kompositor von Chromium eine einzelne „Ebenen“hierarchie, um die geometrische Beziehung der zusammengesetzten Inhalte darzustellen. Diese brach jedoch schnell aus, da die vollständige Komplexität von Funktionen wie „position:fix“ deutlich wurde. In der Ebenenhierarchie wurden zusätzliche nicht lokale Zeiger hinzugefügt, die auf das „scroll Parent“ oder das „clip Parent“ einer Ebene hinweisten, und schnell war der Code sehr schwer zu verstehen.

Dieser Fehler wurde durch Property-Bäume behoben, indem die überlaufenden Scroll- und Clip-Aspekte des Contents getrennt von allen anderen visuellen Effekten dargestellt wurden. Dadurch konnte die tatsächliche visuelle und Scroll-Struktur von Websites korrekt modelliert werden. Als Nächstes mussten wir lediglich Algorithmen auf den Eigenschaftenbäumen implementieren, z. B. die Bildschirm-Raum-Transformation zusammengesetzter Ebenen oder die Bestimmung, welche Ebenen gescrollt haben und welche nicht.

Tatsächlich stellten wir bald fest, dass es viele andere Stellen im Code gab, an denen ähnliche geometrische Fragen gestellt wurden. Eine umfassendere Liste finden Sie im Beitrag zu wichtigen Datenstrukturen. Einige von ihnen hatten doppelte Implementierungen der gleichen Sache, die der Compositor-Code machte. Alle hatten eine andere Teilmenge von Fehlern und keine von ihnen modellierte die tatsächliche Website-Struktur ordnungsgemäß. Die Lösung wurde klar: Sie zentralisieren alle Geometriealgorithmen an einem Ort und refaktorieren den gesamten Code, um ihn zu verwenden.

Diese Algorithmen wiederum hängen von Property-Bäumen ab. Daher sind Property-Bäume eine Schlüssel-Datenstruktur, die in der gesamten Rendering-Pipeline von RenderingNG verwendet wird. Um dieses Ziel des zentralisierten Geometriecodes zu erreichen, mussten wir das Konzept der Eigenschaftsstrukturen viel früher in der Pipeline einführen – als Vormalen – und alle APIs, die jetzt von ihnen abhängig waren, so ändern, dass vor der Ausführung ein Vorstrich ausgeführt werden muss.

Diese Geschichte ist ein weiterer Aspekt des BlinkNG-Refaktorierungsmusters: Schlüsselberechnungen identifizieren, refaktorieren, um doppelte Einträge zu vermeiden, und klar definierte Pipelinephasen erstellen, die die Datenstrukturen bilden, die sie füttern. Wir berechnen Immobilienbäume genau zu dem Zeitpunkt, an dem alle erforderlichen Informationen verfügbar sind, und wir stellen sicher, dass sich die Eigenschaftsbäume während der späteren Rendering-Phasen nicht ändern können.

Verbundstoffe: Lackierung und Compositing

Bei der Layerisierung wird ermittelt, welcher DOM-Inhalt in eine eigene zusammengesetzte Ebene übertragen wird, die wiederum eine GPU-Textur darstellt. Vor dem RenderingNG wurde die Layerisierung vor dem Painting ausgeführt, nicht danach. Informationen zur aktuellen Pipeline findest du hier. Beachten Sie die Änderung der Reihenfolge. Zuerst entscheiden wir, welche Teile des DOM in welche zusammengesetzte Ebene aufgenommen wurden, und erstellen dann erst dann Anzeigelisten für diese Texturen. Natürlich hängten die Entscheidungen davon ab, welche DOM-Elemente animiert oder gescrollt wurden, welche 3D-Transformationen es gab und welche Elemente darüber gemalt wurden.

Dies verursachte erhebliche Probleme, da mehr oder weniger zirkuläre Abhängigkeiten im Code erforderlich waren, was für eine Rendering-Pipeline ein großes Problem darstellt. Anhand eines Beispiels sehen wir uns den Grund dafür an. Angenommen, wir müssen Paint invalidate, d. h. wir müssen die Anzeigeliste neu zeichnen und dann noch einmal rastern. Die Notwendigkeit einer Entwertung kann auf eine Änderung im DOM oder auf einen geänderten Stil oder ein geändertes Layout zurückzuführen sein. Natürlich möchten wir jedoch nur die Teile entwerten, die sich tatsächlich geändert haben. Dazu musste ermittelt werden, welche zusammengesetzten Ebenen betroffen waren, und dann einen Teil oder alle Anzeigelisten für diese Ebenen ungültig machen.

Das bedeutet, dass die Entwertung von DOM, Stil, Layout und früheren Ebenenauswahlentscheidungen (in der Vergangenheit: Bedeutung für den vorherigen gerenderten Frame) abhing. Die aktuelle Ebene hängt jedoch auch von all diesen Faktoren ab. Und da wir nicht zwei Kopien aller Ebenendaten hatten, war es schwierig, den Unterschied zwischen vergangenen und zukünftigen Ebenenentscheidungen zu unterscheiden. Wir hatten schließlich jede Menge Code mit zirkulärer Logik. Dies führte manchmal zu unlogischem oder falschem Code oder sogar zu Abstürzen oder Sicherheitsproblemen, wenn wir nicht sehr vorsichtig waren.

Um mit dieser Situation umzugehen, haben wir zu Beginn das Konzept des DisableCompositingQueryAsserts-Objekts vorgestellt. Wenn der Code versuchte, frühere Ebenenentscheidungen abzufragen, würde dies meistens zu einem Assertion-Fehler führen und den Browser abstürzen, wenn er sich im Debug-Modus befand. Dadurch konnten wir die Einführung neuer Fehler vermeiden. In jedem Fall, in dem der Code legitim erforderlich war, um frühere Ebenenentscheidungen abzufragen, wurde ein DisableCompositingQueryAsserts-Objekt zugewiesen.

Unser Plan bestand darin, nach und nach alle DisableCompositingQueryAssert-Objekte von Anrufwebsites zu entfernen und den Code anschließend als sicher und korrekt zu deklarieren. Wir haben jedoch festgestellt, dass einige der Aufrufe praktisch unmöglich zu entfernen sind, solange die Überlagerung vor dem Painting erfolgte. Schließlich konnten wir sie erst vor Kurzem endgültig entfernen. Dies war der erste Grund, der für das Projekt „Component After Paint“ festgestellt wurde. Wir haben gelernt, dass Sie vielleicht nicht weiterkommen werden, selbst wenn Sie eine gut definierte Pipelinephase für einen Vorgang haben, wenn dieser an der falschen Stelle in der Pipeline steht.

Der zweite Grund für das Composite After Paint-Projekt war der Fehler beim Fundamental Compositing. Eine mögliche Ursache für diesen Fehler ist, dass DOM-Elemente keine gute 1:1-Darstellung eines effizienten oder vollständigen Schichtenschemas für Webseiteninhalte darstellen. Und da die Erstellung noch vor der Farbgestaltung war, hing es mehr oder weniger von DOM-Elementen ab, nicht von Anzeigelisten oder Eigenschaftsstrukturen. Dies ist dem Grund für die Einführung von Property-Bäumen sehr ähnlich. Genau wie bei Property-Bäumen scheitert die Lösung direkt, wenn Sie die richtige Pipelinephase herausfinden, sie zum richtigen Zeitpunkt ausführen und ihr die richtigen wichtigen Datenstrukturen zur Verfügung stellen. Und wie bei den Grundstücksbäumen war dies eine gute Gelegenheit, um sicherzustellen, dass die Ausgabe nach Abschluss der Farbphase für alle nachfolgenden Pipelinephasen unveränderlich ist.

Vorteile

Wie Sie gesehen haben, bringt eine klar definierte Rendering-Pipeline enorme langfristige Vorteile mit sich. Es gibt sogar noch mehr, als Sie vielleicht denken:

  • Erheblich verbesserte Zuverlässigkeit: Das ist ziemlich einfach. Sauberer Code mit klar definierten und verständlichen Benutzeroberflächen ist einfacher zu verstehen, zu schreiben und zu testen. Dadurch wird sie zuverlässiger. Außerdem wird der Code sicherer und stabiler, und es kommt seltener zu Abstürzen und Programmfehlern, die nach der Verwendung behoben werden müssen.
  • Erweiterte Testabdeckung: Im Laufe von BlinkNG haben wir zahlreiche neue Tests hinzugefügt. Dazu gehören Unittests, die eine gezielte Überprüfung der internen Strukturen ermöglichen, Regressionstests, mit denen wir alte Fehler, die wir behoben haben, nicht noch einmal einbauen können (so viele!) und viele Ergänzungen der öffentlichen, gemeinsam verwalteten Web Platform Test Suite, mit der alle Browser die Konformität mit Webstandards messen.
  • Leichter zu erweitern: Wenn ein System in klare Komponenten unterteilt ist, ist es nicht erforderlich, die anderen Komponenten in einem Detail zu verstehen, um in der aktuellen Komponente Fortschritte machen zu können. Dies erleichtert es allen, einen Mehrwert für den Rendering-Code zu schaffen, ohne dass dafür tiefgehende Fachkenntnisse erforderlich sind. Außerdem ist es einfacher, das Verhalten des gesamten Systems nachzuvollziehen.
  • Leistung: Die Optimierung der in Spaghetti-Code geschriebenen Algorithmen ist schwierig genug, aber ohne eine solche Pipeline sind noch größere Dinge wie allgemeines Scrollen und Animationen mit Threads oder Prozesse und Threads für die Website-Isolierung fast unmöglich. Parallelität kann uns helfen, die Leistung enorm zu verbessern, ist aber auch extrem kompliziert.
  • Erträge und Eindämmung: BlinkNG ermöglicht mehrere neue Funktionen, die die Pipeline auf neue und neuartige Weise nutzen. Was aber, wenn die Rendering-Pipeline nur so lange ausgeführt werden soll, bis ein Budget abgelaufen ist? Oder das Rendering für Unterstrukturen überspringen, die derzeit für den Nutzer nicht relevant sind? Und genau das ermöglicht die CSS-Eigenschaft content- visibility. Wie sieht es aus, wenn der Stil einer Komponente von ihrem Layout abhängig ist? Das sind Containerabfragen.

Fallstudie: Containerabfragen

Containerabfragen sind eine mit Spannung erwartete neue Webplattformfunktion. Sie ist seit Jahren die am häufigsten von CSS-Entwicklern angefragte Funktion. Wenn er so toll ist, warum gibt es ihn dann noch nicht? Der Grund dafür ist, dass eine Implementierung von Containerabfragen ein sehr sorgfältiges Verständnis und eine genaue Steuerung der Beziehung zwischen Stil und Layoutcode erfordert. Sehen wir uns das genauer an.

Bei einer Containerabfrage hängen die Stile, die für ein Element gelten, von der Layoutgröße eines übergeordneten Elements ab. Da die Layoutgröße während des Layouts berechnet wird, müssen wir eine Stilneuberechnung nach dem Layout ausführen. Die Stilneuberechnung wird jedoch vor dem Layout ausgeführt! Dieses Huhn-Ei-Pardox ist der einzige Grund, warum wir vor BlinkNG keine Container-Abfragen implementieren konnten.

Wie können wir dieses Problem lösen? Handelt es sich nicht um eine Rückwärtspipeline-Abhängigkeit, d. h. um dasselbe Problem, das bei Projekten wie Composite After Paint gelöst ist? Was ist noch schlimmer, wenn die neuen Stile die Größe des Ancestors ändern? Wird dies nicht manchmal zu einer Endlosschleife führen?

Im Prinzip kann die kreislaufbezogene Abhängigkeit durch die Verwendung der „indhat-CSS“-Eigenschaft gelöst werden, die es ermöglicht, dass das Rendering außerhalb eines Elements nicht vom Rendering innerhalb der Unterstruktur dieses Elements abhängig ist. Das bedeutet, dass sich die neuen Stile, die von einem Container angewendet werden, nicht auf die Größe des Containers auswirken können, da Containerabfragen eine Begrenzung erfordern.

Tatsächlich war das jedoch nicht genug. Es war notwendig, eine schwächere Begrenzung als nur die Größenbeschränkung einzuführen. Dies liegt daran, dass es üblich ist, dass die Größe eines Container-Abfragecontainers basierend auf seinen Inline-Abmessungen nur in eine Richtung (normalerweise blockiert) geändert werden kann. Daher wurde das Konzept der Begrenzungen in Inline-Größe hinzugefügt. Aber wie Sie an der sehr langen Notiz in diesem Abschnitt sehen können, war lange Zeit überhaupt nicht klar, ob eine Begrenzung der Inline-Größe möglich ist.

Es ist eine Sache, die Begrenzung in abstrakter Spezifikationssprache zu beschreiben, und es ist eine andere Sache, sie richtig zu implementieren. Denken Sie daran, dass eines der Ziele von BlinkNG darin bestand, das Prinzip der Begrenzung auf die Baumwanderungen anzuwenden, die die Hauptlogik für das Rendering sind: Beim Durchlaufen eines Unterbaums sollten keine Informationen von außerhalb des Unterbaums erforderlich sein. Es ist aber nicht gerade ein Zufall. Die CSS-Begrenzung ist viel einfacher und übersichtlicher zu implementieren, wenn der Renderingcode das Begrenzungsprinzip einhält.

Zukunft: Zusammensetzung außerhalb des Hauptthreads ... und darüber hinaus!

Die hier dargestellte Rendering-Pipeline ist der aktuellen RenderingNG-Implementierung etwas voraus. Die Ebenenstruktur wird so angezeigt, als würde sie sich außerhalb des Hauptthreads befinden, während sie sich derzeit noch im Hauptthread befindet. Es ist jedoch nur eine Frage der Zeit, bis dies fertig ist. Jetzt ist der Verbundstoff nach Paint versandt und die Schichtung nach der Farbe.

Um zu verstehen, warum dies wichtig ist und wohin dies sonst noch führen kann, müssen wir die Architektur der Rendering-Engine von einem etwas höheren Punkt aus betrachten. Eines der langlebigsten Hürden bei der Verbesserung der Leistung von Chromium ist die einfache Tatsache, dass der Hauptthread des Renderers sowohl die Hauptanwendungslogik (d. h. das Ausführen eines Skripts) als auch den Großteil des Renderings verarbeitet. Daher ist der Hauptthread häufig mit Arbeit übersät. Eine Überlastung des Hauptthreads stellt häufig einen Engpass im gesamten Browser dar.

Die gute Nachricht ist, dass das nicht so sein muss. Dieser Aspekt der Chromium-Architektur geht zurück bis in die KHTML-Tage, als die Single-Thread-Ausführung das dominante Programmiermodell war. Als Multi-Core-Prozessoren in Verbrauchergeräten zum Standard wurden, war die Single-Threaded-Annahme fest in Blink (zuvor WebKit) integriert. Wir wollten schon lange mehr Threading in das Rendering-Modul einbauen, aber dies war mit dem alten System einfach unmöglich. Eines der Hauptziele von Rendering NG war es, uns aus diesem Loch herauszuholen und Rendering-Arbeiten teilweise oder insgesamt in einen oder mehrere andere Threads zu verschieben.

Da BlinkNG fast fertig ist, fangen wir bereits an, diesen Bereich zu untersuchen. Non-Blocking Commit ist ein erster Schritt zur Änderung des Threading-Modells des Renderers. Ein Compositor-Commit (oder einfach commit) ist ein Synchronisierungsschritt zwischen dem Hauptthread und dem Compositor-Thread. Während des Commits erstellen wir Kopien von Renderingdaten, die im Hauptthread erzeugt werden und vom nachgelagerten Compositing-Code verwendet werden, der im Compositor-Thread ausgeführt wird. Während dieser Synchronisierung wird die Ausführung des Hauptthreads angehalten, während der Kopiercode im Compositor-Thread ausgeführt wird. Dadurch wird sichergestellt, dass der Hauptthread seine Renderingdaten nicht ändert, während der Compositor-Thread sie kopiert.

Mit einem nicht blockierenden Commit muss der Hauptthread nicht mehr beendet und auf das Ende der Commit-Phase gewartet werden. Der Hauptthread arbeitet weiter, während der Commit im Compositor-Thread gleichzeitig ausgeführt wird. Der Nettoeffekt eines nicht blockierenden Commits ist eine Reduzierung der Zeit, die für das Rendern des Hauptthreads benötigt wird, was die Überlastung des Hauptthreads verringert und die Leistung verbessert. Zum Zeitpunkt dieses Artikels (März 2022) haben wir einen funktionierenden Prototyp für den Non-Blocking Commit und bereiten uns derzeit auf eine detaillierte Analyse der Auswirkungen auf die Leistung vor.

In den Flügeln steht Off-Main-Thread Compositing zur Verfügung. Damit soll die Rendering-Engine an die Abbildung angepasst werden, indem die Layerisierung vom Hauptthread in einen Worker-Thread verschoben wird. Wie bei „Non-Blocking Commit“ reduziert dies die Überlastung des Hauptthreads, indem die Rendering-Arbeitslast reduziert wird. Ein solches Projekt wäre ohne die architektonischen Verbesserungen von Composite After Paint nicht möglich gewesen.

Und es sind noch weitere Projekte in Arbeit! Wir haben endlich eine Grundlage, die es ermöglicht, mit der Neuverteilung von Rendering-Arbeiten zu experimentieren, und wir sind sehr gespannt, was möglich ist!