RenderingNG im Detail: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

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

Blink begann als Abzweigung des WebKit, das wiederum eine Abspaltung von KHTML aus dem Jahr 1998 ist. Sie enthält einige der ältesten (und kritischsten) Codes in Chromium, und im Jahr 2014 zeigte er definitiv sein Alter. In diesem Jahr haben wir unter dem Banner des sogenannten BlinkNG eine Reihe ehrgeiziger Projekte in Angriff genommen, um langjährige Mängel in der Organisation und der Struktur des Blink-Codes zu beheben. In diesem Artikel werden BlinkNG und die zugehörigen Projekte untersucht: warum wir sie durchgeführt haben, was sie erreicht haben, die Leitprinzipien für ihr Design und die Möglichkeiten für zukünftige Verbesserungen, die sie sich bieten.

Die Rendering-Pipeline vor und nach BlinkNG.

Rendering vor NG

Die Rendering-Pipeline in Blink war immer konzeptionell in Phasen unterteilt (style, layout, paint usw.), aber die Abstraktionsbarrieren waren undicht. Im Großen und Ganzen waren die mit dem Rendering verbundenen Daten aus langlebigen, veränderlichen Objekten. Diese Objekte konnten jederzeit modifiziert werden und wurden häufig recycelt und durch aufeinanderfolgende Rendering-Updates wiederverwendet. Es war unmöglich, einfache Fragen wie die folgenden zu beantworten:

  • Muss die Ausgabe für Stil, Layout oder Farbe aktualisiert werden?
  • Wann erhalten diese Daten ihre "endgültigen" Wert?
  • Wann können diese Daten geändert werden?
  • Wann wird dieses Objekt gelöscht?

Dazu gibt es zahlreiche Beispiele:

Style generiert ComputedStyles auf Basis von Stylesheets. aber ComputedStyle war nicht unveränderlich. in einigen Fällen durch spätere Pipelinephasen geändert werden würde.

Mit Style wird die Baumstruktur LayoutObject generiert. layout versieht diese Objekte dann mit Größen- und Positionierungsinformationen. In einigen Fällen würde das Layout sogar die Baumstruktur verändern. Bei layout wurden die Ein- und Ausgaben nicht klar voneinander getrennt.

Mit Stil würden Zubehördatenstrukturen generiert, die den Verlauf der Zusammensetzung bestimmen. Diese Datenstrukturen wurden von jeder Phase nach dem Stil übernommen.

Auf einer niedrigeren Ebene bestehen Rendering-Datentypen hauptsächlich aus speziellen Baumstrukturen (z. B. DOM-Baum, Stilbaum, Layoutbaum, Paint-Eigenschaftsbaum). und Rendering-Phasen werden als rekursive Baumwanderungen implementiert. Idealerweise sollte eine Baumstruktur enthält sein: Bei der Verarbeitung eines bestimmten Baumknotens sollten wir auf keine Informationen außerhalb der Unterstruktur, die in diesem Knoten gespeichert ist, zugreifen. Das trifft vor dem RenderingNG nie zu. Tree Walks führt häufig aufgerufene Informationen von Ancestors des zu verarbeitenden Knotens durch. Das machte das System sehr fragil und fehleranfällig. Außerdem war es unmöglich, einen Baumspaziergang von überall aus als von der Wurzel des Baums aus zu starten.

Schließlich gab es viele Auffahrten/Ausfahrten in die Rendering-Pipeline im Code: erzwungene Layouts, die durch JavaScript ausgelöst wurden, Teilaktualisierungen, die während des Ladens von Dokumenten ausgelöst wurden, erzwungene Aktualisierungen zur Vorbereitung auf das Ereignis-Targeting, geplante Aktualisierungen, die vom Anzeigesystem angefordert wurden, und spezielle APIs, die nur zum Testen von Code freigegeben wurden, um nur einige zu nennen. Es gab sogar einige rekursive und wiederkehrende Pfade in der Rendering-Pipeline, d. h., eine Phase wurde von der Mitte einer anderen an den Anfang gesprungen. Jede dieser Verbindungsrampen hatte ihr eigenes idiosynchrones Verhalten. In einigen Fällen hängt die Ausgabe des Rendering-Prozesses davon ab, wie die Rendering-Aktualisierung ausgelöst wurde.

Was wir geändert haben

BlinkNG besteht aus vielen großen und kleinen Teilprojekten mit dem gemeinsamen Ziel, die zuvor beschriebenen Architekturmängel zu beseitigen. Diese Projekte umfassen einige Grundprinzipien, die die Rendering-Pipeline zu einer echten Pipeline machen:

  • Einheitlicher Einstiegspunkt: Die Pipeline sollte immer am Anfang eingegeben werden.
  • Funktionsphasen: Jede Phase sollte klar definierte Ein- und Ausgaben haben und ihr Verhalten sollte funktional sein, d. h. deterministisch und wiederholbar sein. Die Ausgaben sollten nur von den definierten Eingaben abhängen.
  • Konstante Eingaben: Die Eingaben jeder Phase sollten praktisch konstant sein, während die Phase ausgeführt wird.
  • Unveränderliche Ausgaben: Sobald eine Phase beendet ist, sollten ihre Ausgaben für den Rest des Rendering-Updates unveränderlich sein.
  • Prüfpunktkonsistenz: Am Ende jeder Phase sollten die bisher erzeugten Renderingdaten in einem inkonsistenten Zustand sein.
  • Deduplizierung der Arbeit: Jedes Element wird nur einmal berechnet.

Eine vollständige Liste der BlinkNG-Teilprojekte würde mühsam lesen, aber im Anschluss sind einige besondere Konsequenzen.

Lebenszyklus des Dokuments

Die Klasse DocumentLifecycle erfasst unseren Fortschritt durch die Rendering-Pipeline. Damit können grundlegende Prüfungen durchgeführt werden, um die zuvor aufgeführten Invarianten zu erzwingen, wie zum Beispiel:

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

Im Rahmen der Implementierung von BlinkNG haben wir systematisch Codepfade entfernt, die gegen diese Invarianten verstoßen, und viele weitere Assertions in den Code verteilt, damit wir nicht zurückbleiben.

Wenn Sie sich schon einmal einen 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 umfasste dies rekursive und nicht eintretende Aufrufpfade sowie Stellen, an denen wir in einer Zwischenphase die Pipeline betreten haben, anstatt von vorn zu beginnen. Im Rahmen von BlinkNG haben wir diese Aufrufpfade analysiert und festgestellt, dass sie sich alle auf zwei grundlegende Szenarien reduzieren lassen:

  • Alle Renderingdaten müssen aktualisiert werden, beispielsweise beim Generieren neuer Pixel für die Anzeige oder bei einem Treffertest für das Ereignis-Targeting.
  • Wir benötigen einen aktuellen Wert für eine bestimmte Suchanfrage, der beantwortet werden kann, ohne alle Rendering-Daten zu aktualisieren. Dazu gehören die meisten JavaScript-Abfragen, z. B. node.offsetTop.

Jetzt gibt es nur noch zwei Einstiegspunkte in die Rendering-Pipeline, die diesen beiden Szenarien entsprechen. Die eintretenden Codepfade wurden entfernt oder refaktoriert und es ist nicht mehr möglich, in einer Zwischenphase in die Pipeline zu gelangen. Dadurch ergaben sich viele Unklarheiten darüber, wann und wie Rendering-Updates durchgeführt werden, was das Verhalten des Systems wesentlich einfacher zu verstehen macht.

Pipelines-Stil, Layout und Voranstrich

Insgesamt sorgen die Renderingphasen vor paint für Folgendes:

  • Stilkaskaden-Algorithmus ausführen, um endgültige Stileigenschaften für DOM-Knoten zu berechnen
  • Generieren des Layoutbaums, der die Feldhierarchie des Dokuments darstellt
  • Größen- und Positionsinformationen für alle Boxen werden ermittelt.
  • Runden oder Andocken der Subpixel-Geometrie an ganze Pixelgrenzen für das Painting.
  • Eigenschaften zusammengesetzter Schichten bestimmen (affine Transformation, Filter, Deckkraft oder alles andere, was GPU-beschleunigt werden kann).
  • Feststellen, welche Inhalte sich seit der vorherigen Farbphase geändert haben und überstrichen oder neu gezeichnet werden müssen (Entwertung der Farbe).

An dieser Liste hat sich nichts geändert, aber vor BlinkNG wurde ein Großteil dieser Arbeit ad hoc durchgeführt, verteilt über mehrere Rendering-Phasen, mit vielen duplizierten Funktionen und integrierten Ineffizienzen. Beispielsweise war die style-Phase in erster Linie für die Berechnung der endgültigen Stileigenschaften für Knoten verantwortlich. Es gab jedoch einige Sonderfälle, in denen die endgültigen Werte für Stileigenschaften erst nach Abschluss der style-Phase festgelegt wurden. Im Renderingprozess gab es keinen formellen 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 Ungültigkeit von Farben. Früher wurde die Entwertung von Paint über alle Rendering-Phasen bis zur Paint-Phase verteilt. Bei der Modifizierung von Stil oder Layoutcode war es schwierig, zu erkennen, welche Änderungen an der Entwertungslogik erforderlich waren, und es war leicht, einen Fehler zu machen, der zu Fehlern bei der Unter- oder Überentwertung führte. Weitere Informationen zu den Feinheiten des alten Systems zur Entwertung von Farben finden Sie in dem Artikel aus dieser Reihe zum Thema LayoutNG.

Das Andocken der Subpixel-Layoutgeometrie an ganze Pixelgrenzen zum Malen ist ein Beispiel dafür, wo wir mehrere Implementierungen derselben Funktionalität hatten und viele redundante Arbeit geleistet haben. Das Paint-System verwendete einen Codepfad zum Ausschneiden von Pixeln und einen völlig separaten Codepfad, der immer dann verwendet wurde, wenn eine einmalige On-the-Fly-Berechnung von mit Pixeln geschobenen Koordinaten außerhalb des Paint-Codes erforderlich war. Natürlich hatte jede Implementierung Fehler und die Ergebnisse stimmten nicht immer überein. Da diese Informationen nicht im Cache gespeichert wurden, führte das System manchmal genau die gleiche Berechnung wiederholt durch – eine weitere Belastung der Leistung.

Hier sind einige wichtige Projekte, bei denen die Architekturmängel der Renderingphasen vor dem Paint beseitigt wurden.

Project Squad: Pipelining der Stilphase

Bei diesem Projekt wurden in der Stilphase zwei wesentliche Defizite aus dem Weg geräumt, die eine saubere Pipeline-Entwicklung verhinderten:

Es gibt zwei primäre Ausgaben der Stilphase: ComputedStyle, die das Ergebnis der Ausführung des CSS-Kaskadenalgorithmus über den DOM-Baum enthält. und einer Baumstruktur von LayoutObjects, die die Reihenfolge der Vorgänge für die Layoutphase festlegt. Konzeptionell sollte das Ausführen des Cascade-Algorithmus ausschließlich vor der Generierung des Layoutbaums erfolgen. aber vorher waren diese beiden Operationen verschränkt. Project Squad gelang es, diese beiden in separate, sequenzielle Phasen aufzuteilen.

Bisher hat ComputedStyle bei der Stilneuberechnung nicht immer den endgültigen Wert erhalten. wurde ComputedStyle in einigen Situationen in einer späteren Pipelinephase aktualisiert. Diese Codepfade wurden in Project Squad erfolgreich refaktoriert, sodass ComputedStyle nach der Stilphase nicht mehr geändert wird.

LayoutNG: Pipelining der Layoutphase

Dieses umfassende Projekt – einer der Eckpfeiler des RenderingNGs – war eine vollständige Neuschreibung der Layout-Rendering-Phase. 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, in der die Baumstruktur mit Informationen zu Größe und Position annotiert wurde. Daher gab es keine klare Trennung der Eingaben und Ausgaben. LayoutNG hat die Fragmentstruktur eingeführt. Sie ist die primäre, schreibgeschützte Ausgabe des Layouts und dient als primäre Eingabe für nachfolgende Rendering-Phasen.
  • LayoutNG hat die Begrenzungseigenschaft in das Layout "layout" aufgenommen: Bei der Berechnung der Größe und Position eines bestimmten LayoutObject-Objekts 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 vorher berechnet und als schreibgeschützte Eingabe für den Algorithmus bereitgestellt.
  • Zuvor gab es Grenzfälle, in denen der Layout-Algorithmus nicht genau funktionierte: Das Ergebnis des Algorithmus hing vom letzten vorherigen Layout-Update ab. LayoutNG hat diese Fälle beseitigt.

Die Voranstrichphase

Zuvor gab es keine formale Voranmal-Renderingphase, sondern nur eine Fülle von Operationen nach dem Layout. Die Phase Voranstrich entstand aus der Erkenntnis, dass es einige verwandte Funktionen gab, die am besten als systematischer Durchlauf des Layoutbaums nach Fertigstellung des Layouts implementiert werden konnten. und das Wichtigste:

  • Entwertung von Farben ausgeben: Wenn uns unvollständige Informationen vorliegen, ist es sehr schwierig, die Farbentwertung während des Layouts korrekt durchzuführen. Es ist viel einfacher, richtig zu liegen, und kann sehr effizient sein, wenn es in zwei verschiedene Prozesse aufgeteilt wird: Während des Stils und des Layouts können Inhalte mit einem einfachen booleschen Flag mit dem Hinweis „kann möglicherweise entwertet werden“ markiert werden. Beim Voranstreichen der Baumstruktur überprüfen wir diese Meldungen und führen bei Bedarf die Entwertung aus.
  • Paint-Eigenschaftsstrukturen generieren: Ein Prozess, der weiter unten ausführlicher beschrieben wird.
  • Berechnen und Aufzeichnen von pixelbasierten Paint-Positionen: Die aufgezeichneten Ergebnisse können ohne redundante Berechnung von der Paint-Phase und auch von jedem nachgelagerten Code verwendet werden, der sie benötigt.

Eigenschaftsbäume: Einheitliche Geometrie

Property-Bäume wurden zu Beginn des RenderingNGs eingeführt, um das komplexe Scrollen zu vereinfachen, das im Web eine andere Struktur aufweist als alle anderen Arten von visuellen Effekten. Vor Eigentumsbäumen verwendete der Chromium-Kompositor eine einzelne "Schicht" eine Hierarchie zur Darstellung der geometrischen Beziehung von zusammengesetzten Inhalten. Diese fiel jedoch schnell auseinander, als die Komplexität von Funktionen wie „position:fix“ deutlich wurde. In der Ebenenhierarchie wurden zusätzliche nicht lokale Zeiger hinzugefügt, die auf das übergeordnete Scrollelement hinweist. oder „Übergeordneter Clip“ Schon nach kurzer Zeit war es sehr schwierig, den Code zu verstehen.

Dieses Problem wurde durch die Eigenschaftenstrukturen behoben, bei denen die Scroll- und Clip-Aspekte des Überlaufs des Inhalts getrennt von allen anderen visuellen Effekten dargestellt wurden. Dadurch war es möglich, die echte visuelle und scrollbare Struktur von Websites richtig nachzubilden. Als Nächstes: „all“ mussten wir Algorithmen über den Eigenschaftsbäumen implementieren, z. B. die Bildschirmraumtransformation von zusammengesetzten Ebenen oder bestimmen, welche Schichten gescrollt wurden und welche nicht.

Tatsächlich stellten wir schnell fest, dass es viele andere Stellen im Code gab, an denen ähnliche geometrische Fragen aufgestellt wurden. Eine ausführlichere Liste finden Sie im Beitrag zu den Schlüsseldatenstrukturen. Einige von ihnen enthielten doppelte Implementierungen der gleichen Aktion wie der Compositor-Code. hatten alle unterschiedliche Fehlergruppen, und keine davon hat die tatsächliche Website-Struktur richtig modelliert. Die Lösung wurde klar: Alle Geometriealgorithmen an einem Ort zentralisieren und den gesamten Code für die Verwendung refaktorieren.

Diese Algorithmen wiederum sind von Eigenschaftsbäumen abhängig. Daher sind Eigenschaftsbäume eine Schlüssel-Datenstruktur, d. h. eine, die in der gesamten Pipeline von RenderingNG verwendet wird. Um dieses Ziel des zentralisierten Geometriecodes zu erreichen, mussten wir also das Konzept der Eigenschaftsbäume viel früher in der Pipeline einführen – in der Voranstrich-Methode – und alle APIs, die jetzt davon abhängig waren, so geändert, dass vor der Ausführung eine Vormalung ausgeführt werden musste.

Diese Geschichte ist ein weiterer Aspekt des BlinkNG-Refaktorierungsmusters: Identifizierung wichtiger Berechnungen, Refaktorierung, um Duplikate zu vermeiden, und klar definierte Pipeline-Phasen zu erstellen, die die Datenstrukturen schaffen, die sie versorgen. Wir berechnen Eigenschaftsbäume genau zu dem Zeitpunkt, an dem alle erforderlichen Informationen verfügbar sind. Außerdem stellen wir sicher, dass sich die Eigenschaftsbäume nicht ändern können, während spätere Rendering-Phasen ausgeführt werden.

Verbund nach Paint: Pipelining-Farbe und Compositing

Bei der Layerisierung wird ermittelt, welche DOM-Inhalte in eine eigene zusammengesetzte Ebene eingefügt werden, die wiederum eine GPU-Textur darstellt. Vor dem RenderingNG wurde die Ebenenerstellung vor dem Paint ausgeführt, nicht nachher. Die aktuelle Pipeline finden Sie hier. Beachten Sie die Reihenfolge der Änderungen. Wir würden zuerst entscheiden, welche Teile des DOMs in welche zusammengesetzte Ebene aufgenommen werden, und erst dann Anzeigelisten für diese Texturen erstellen. Natürlich hing die Entscheidung von Faktoren ab, z. B. welche DOM-Elemente animiert wurden oder gescrollt wurden oder welche 3D-Transformationen verwendet wurden und welche Elemente darüber gemalt wurden.

Dies führte zu großen Problemen, da mehr oder weniger zirkuläre Abhängigkeiten im Code erforderlich waren, was für eine Rendering-Pipeline ein großes Problem darstellt. Sehen wir uns das anhand eines Beispiels genauer an. Angenommen, wir müssen Paint entwerten, d. h., wir müssen die Anzeigeliste neu zeichnen und dann noch einmal rastern. Die Notwendigkeit einer Entwertung kann durch eine Änderung im DOM oder durch einen geänderten Stil oder Layout verursacht werden. Aber natürlich möchten wir nur die Teile entwerten, die sich tatsächlich geändert haben. Dies bedeutete, herauszufinden, welche zusammengesetzten Ebenen betroffen waren, und dann einen Teil oder alle Anzeigelisten für diese Ebenen ungültig zu machen.

Das bedeutet, dass die Entwertung von DOM-, Stil-, Layout- und früheren Layer-Entscheidungen abhängig war (vergangen: Bedeutung für den vorherigen gerenderten Frame). Aber die aktuelle Layerisierung hängt auch von all diesen Dingen ab. Und da wir nicht zwei Kopien aller Layering-Daten hatten, war es schwierig, den Unterschied zwischen früheren und zukünftigen Layer-Entscheidungen zu erkennen. Wir erhielten also jede Menge Code mit zirkulären Schlussfolgerungen. Dies führte manchmal zu unlogischem oder falschen Code, 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 eingeführt. In den meisten Fällen würde es zu einem Assertion-Fehler und zum Absturz des Browsers im Debug-Modus kommen, wenn Code versucht, vergangene Layerization-Entscheidungen abzufragen. So konnten wir neue Fehler vermeiden. Und in jedem Fall, in dem der Code rechtmäßig frühere Layer-Entscheidungen abfragen muss, fügen wir Code hinzu, um dies zu ermöglichen, indem wir ein DisableCompositingQueryAsserts-Objekt zuweisen.

Unser Plan war, im Laufe der Zeit alle DisableCompositingQueryAssert-Objekte für Aufrufwebsites zu entfernen und dann den Code für sicher und korrekt zu deklarieren. Wir haben jedoch herausgefunden, dass eine Reihe der Aufrufe im Grunde unmöglich entfernt werden konnten, solange die Layering vor dem Paint erfolgte. (erst kürzlich konnten wir sie entfernen.) Dies war der erste Grund, der für das Projekt „Composite After Paint“ entdeckt wurde. Was wir gelernt haben: Selbst wenn Sie eine klar definierte Pipelinephase für einen Vorgang haben, werden Sie letztendlich hängen bleiben, wenn er sich an der falschen Stelle in der Pipeline befindet.

Der zweite Grund für das Composite After Paint-Projekt war der Fehler beim Fundamental Compositing. Eine Möglichkeit, diesen Fehler zu betonen, besteht darin, dass DOM-Elemente keine gute 1:1-Darstellung eines effizienten oder vollständigen Layering-Schemas für Webseiteninhalte sind. Und da die Zusammensetzung vor der Farbüberarbeitung erfolgte, hing sie mehr oder weniger von Grund auf von DOM-Elementen ab, nicht von Anzeigelisten oder Eigenschaftsbäumen. Dies ähnelt dem Grund, aus dem wir Property-Bäume eingeführt haben. Wie bei Property-Bäumen fällt die Lösung direkt aus, wenn Sie die richtige Pipelinephase finden, sie zur richtigen Zeit ausführen und die richtigen Schlüsseldatenstrukturen bereitstellen. Und wie bei Property-Bäumen war dies eine gute Gelegenheit, um sicherzustellen, dass die Ausgabe nach Abschluss der Paint-Phase für alle nachfolgenden Pipelinephasen unveränderlich ist.

Vorteile

Wie Sie gesehen haben, bietet eine gut definierte Rendering-Pipeline auf lange Sicht enorme Vorteile. Es gibt noch mehr, als Sie vielleicht denken:

  • Deutlich verbesserte Zuverlässigkeit: Diese Option ist recht unkompliziert. Sauberer Code mit klar definierten und verständlichen Benutzeroberflächen ist leichter zu verstehen, zu schreiben und zu testen. Dies macht sie zuverlässiger. Außerdem wird der Code sicherer und stabiler und es kommt seltener zu Abstürzen und weniger Fehlern, die nach der Verwendung nach dem Start des Programms nicht mehr verwendet werden müssen.
  • Erweiterte Testabdeckung: Im Rahmen von BlinkNG haben wir viele neue Tests zu unserer Suite hinzugefügt. Dazu gehören Unittests, die eine gezielte Überprüfung der internen Strukturen ermöglichen. Regressionstests, die uns daran hindern, bereits behobene Fehler wieder einzuführen (so viele!); Außerdem gibt es viele neue, öffentlich zugängliche Web Platform-Testsuites, mit denen alle Browser die Konformität mit Webstandards messen.
  • Leichter zu erweitern: Wenn ein System in klare Komponenten unterteilt ist, ist es nicht notwendig, die anderen Komponenten im Detail zu verstehen, um mit dem aktuellen System fortzufahren. So können alle einen Mehrwert für den Rendering-Code schaffen, ohne dafür tiefgreifende Fachkenntnisse zu benötigen. Außerdem wird es einfacher, das Verhalten des gesamten Systems zu beurteilen.
  • Leistung: Die Optimierung der in Spaghetticode geschriebenen Algorithmen ist schwierig genug, aber ohne eine solche Pipeline ist es fast unmöglich, noch größere Dinge wie universelles Scrollen und Animationen mit universellen Threads oder die Prozesse und Threads zur Website-Isolierung zu erreichen. Parallelität kann die Leistung enorm verbessern, ist aber auch äußerst kompliziert.
  • Förderung und Eindämmung: BlinkNG bietet mehrere neue Funktionen, die die Pipeline auf neue und neuartige Weise trainieren. Was ist beispielsweise, wenn wir die Rendering-Pipeline nur ausführen möchten, bis ein Budget abgelaufen ist? Oder soll das Rendern für Unterstrukturen übersprungen werden, die für den Nutzer aktuell nicht relevant sind? Dies ermöglicht die CSS-Eigenschaft content- visibility. Wie wäre es, 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 nachgefragte Funktion von Preisvergleichsportal-Entwicklern. Wenn sie so toll ist, warum gibt es sie dann noch nicht? Der Grund dafür ist, dass eine Implementierung von Containerabfragen ein sehr sorgfältiges Verständnis und eine gute Kontrolle der Beziehung zwischen Stil und Layoutcode erfordert. Sehen wir uns das genauer an.

Bei einer Containerabfrage können die Stile, die auf ein Element angewendet werden, von der Layoutgröße eines Ancestors abhängen. Da die Layoutgröße während des Layouts berechnet wird, bedeutet dies, dass wir nach dem Layout eine Neuberechnung des Stils durchführen müssen. aber die Stilneuberechnung wird vor dem Layout ausgeführt! Dieses Huhn-und-Ei-Pardox ist der einzige Grund, warum wir vor BlinkNG keine Containerabfragen implementieren konnten.

Wie können wir dieses Problem lösen? Ist es nicht eine umgekehrte Pipelineabhängigkeit, d. h. dasselbe Problem, das durch Projekte wie Composite After Paint gelöst wurde? Was noch schlimmer ist, wenn die neuen Stile die Größe des Vorgängers ändern? Führt dies nicht manchmal zu einer Endlosschleife?

Prinzipiell kann die kreisförmige Abhängigkeit durch die Verwendung der CSS-Eigenschaft "include" gelöst werden, die dafür sorgt, dass das Rendering außerhalb eines Elements nicht vom Rendering innerhalb der Unterstruktur dieses Elements abhängig ist. Das bedeutet, dass die neuen Stile, die von einem Container angewendet werden, keinen Einfluss auf die Größe des Containers haben, da Containerabfragen eine Begrenzung erfordern.

Das reichte jedoch nicht aus und es musste eine schwächere Art der Begrenzung eingeführt werden, als nur die Größe. Das liegt daran, dass die Größe eines Containers mit Containerabfragen oft nur in einer Richtung (normalerweise Block) basierend auf den Inline-Abmessungen angepasst werden kann. Daher wurde das Konzept der Begrenzungen in Inline-Größe hinzugefügt. Aber wie Sie der sehr langen Notiz in diesem Abschnitt entnehmen können, war lange Zeit überhaupt nicht klar, ob eine Begrenzung der Inline-Größe möglich war.

Es ist eine Sache, die Begrenzung in abstrakter Spezifikationssprache zu beschreiben, und es ist etwas ganz anderes, sie richtig zu implementieren. Denken Sie daran, dass eines der Ziele von BlinkNG darin bestand, das Begrenzungsprinzip auf die Baumbegehungen anzuwenden, die die Hauptlogik des Renderings bilden: Beim Durchlaufen einer Unterstruktur sollten keine Informationen von außerhalb der Unterstruktur erforderlich sein. Sonst ist es viel sauberer und einfacher, die CSS-Begrenzung zu implementieren, wenn der Rendering-Code dem Prinzip der Begrenzung entspricht.

Zukünftig: Compositing außerhalb des Hauptthreads... und darüber hinaus!

Die hier gezeigte Rendering-Pipeline ist der aktuellen RenderingNG-Implementierung etwas voraus. Die Layerisierung wird als außerhalb des Hauptthreads angezeigt, während sie sich derzeit noch im Hauptthread befindet. Es ist jedoch nur eine Frage der Zeit.

Um zu verstehen, warum dies wichtig ist und wohin es sonst noch führen könnte, müssen wir die Architektur der Rendering-Engine von einem etwas höheren Blickwinkel aus betrachten. Eines der langlebigsten Hindernisse bei der Verbesserung der Leistung von Chromium ist die einfache Tatsache, dass der Hauptthread des Renderers sowohl die Hauptanwendungslogik (das Ausführen des Skripts) als auch den Großteil des Renderings verarbeitet. Daher ist der Hauptthread häufig mit Arbeit überlastet und eine Überlastung des Hauptthreads ist häufig der Engpass im gesamten Browser.

Die gute Nachricht ist, dass das nicht so sein muss. Dieser Aspekt der Chromium-Architektur reicht bis in die KHTML zurück, als die Single-Threaded-Ausführung das dominante Programmiermodell war. Als Mehrkernprozessoren in Verbrauchergeräten zum Standard wurden, war die Single-Threaded-Annahme bereits in Blink (früher WebKit) fest integriert. Wir wollten schon lange mehr Threading in die Rendering-Engine einbinden, aber im alten System war dies einfach unmöglich. Eines der Hauptziele des Renderings von NG bestand darin, uns aus dieser Lücke herauszuholen und Renderingarbeiten ganz oder teilweise in einen anderen Thread (oder Threads) zu verschieben.

BlinkNG nähert sich der Fertigstellung und wir fangen bereits an, diesen Bereich zu erforschen. Das Non-Blocking Commit ist ein erster Versuch, das Threading-Modell des Renderers zu ändern. Ein Compositor-Commit (oder einfach commit) ist ein Synchronisierungsschritt zwischen dem Hauptthread und dem Compositor-Thread. Während des Commits erstellen wir Kopien von Rendering-Daten, die im Hauptthread erstellt werden, um sie vom nachgelagerten Compositing-Code zu verwenden, der auf dem Compositor-Thread ausgeführt wird. Während dieser Synchronisierung wird die Ausführung des Hauptthreads angehalten, während der Kopiercode auf dem zusammengesetzten Thread ausgeführt wird. Dies geschieht, um sicherzustellen, dass der Hauptthread seine Renderingdaten nicht ändert, während der Compositor-Thread sie kopiert.

Ein Non-Blocking-Commit macht es überflüssig, den Hauptthread nicht zu stoppen und auf das Ende der Commit-Phase zu warten. Der Hauptthread wird weiterarbeiten, während der Commit gleichzeitig auf dem Commitor-Thread ausgeführt wird. Der Nettoeffekt des Non-Blocking-Commits ist eine Reduzierung des Zeitaufwands für das Rendern der Arbeit im Hauptthread, wodurch die Überlastung des Hauptthreads verringert und die Leistung verbessert wird. Derzeit (März 2022) haben wir einen funktionierenden Prototyp des nicht blockierenden Commits und bereiten eine detaillierte Analyse der Auswirkungen auf die Leistung vor.

Das Warten in den Flügeln ist Off-Main-Thread-Compositing, mit dem die Rendering-Engine an die Illustration angepasst werden soll, indem die Layerization vom Hauptthread in einen Worker-Thread verschoben wird. Wie das Non-Blocking-Commit wird auch hier die Überlastung des Hauptthreads reduziert, indem die Rendering-Arbeitslast reduziert wird. Ein solches Projekt wäre ohne die architektonischen Verbesserungen von Composite After Paint nie möglich gewesen.

Und es sind weitere Projekte in Planung (Wortspiel beabsichtigt)! Wir haben endlich eine Grundlage, die es uns ermöglicht, mit der Neuverteilung von Rendering-Arbeiten zu experimentieren, und wir sind gespannt, was alles möglich ist.