RenderingNG im Detail: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Ich bin Ian Kilpatrick, Engineering Lead im Blink-Layout-Team, zusammen mit Koji Ishii. Bevor ich im Blink-Team gearbeitet habe, war ich Frontend-Entwickler (bevor Google die Rolle des Frontend-Entwicklers hatte) und Funktionen in Google Docs, Drive und Gmail entwickelten. Nach etwa fünf Jahren in dieser Rolle wechselte ich in ein großes Spiel zum Blink-Team, lernte C++ effektiv bei der Arbeit und versuchte, die extrem komplexe Blink-Codebasis voranzutreiben. Selbst heute verstehe ich nur einen relativ kleinen Teil davon. Ich bin dankbar für die Zeit, die ich mir in dieser Zeit gegeben habe. Ich habe mich gefreut darüber, dass vor mir viele "wiederholte Frontend-Entwickler" zum Browser-Entwickler gewechselt sind.

Meine bisherigen Erfahrungen haben mich als Blink-Team persönlich begleitet. Als Frontend-Entwickler bin ich ständig auf Browser-Inkonsistenzen, Leistungsprobleme, Rendering-Fehler und fehlende Funktionen gestoßen. LayoutNG bot mir die Chance, diese Probleme systematisch im Layoutsystem von Blink zu lösen. Dies ist die Summe der Bemühungen, die viele Entwickler über die Jahre hinweg geleistet haben.

In diesem Beitrag erkläre ich, wie große Architekturänderungen wie diese verschiedene Arten von Fehlern und Leistungsproblemen reduzieren und mindern können.

Eine Ansicht aus 9.000 m Höhe über Layout-Engine-Architekturen

Früher habe ich die Layoutstruktur von Blink als "änderbare Baumstruktur" bezeichnet.

Zeigt die Struktur wie im folgenden Text beschrieben an.

Jedes Objekt in der Layoutstruktur enthielt Eingabeinformationen wie die verfügbare Größe eines übergeordneten Objekts, die Position von Gleitkommazahlen und Ausgabeinformationen, z. B. die endgültige Breite und Höhe des Objekts oder seine x- und y-Position.

Diese Objekte wurden zwischen den Renderings herum beibehalten. Bei einer Stiländerung haben wir das Objekt als „schmutzig“ markiert, ebenso wie alle übergeordneten Elemente. In der Layoutphase der Rendering-Pipeline bereinigten wir den Baum, führen alle schmutzigen Objekte durch und führten dann das Layout aus, um sie in einen bereinigten Zustand zu bringen.

Wir haben festgestellt, dass diese Architektur zu vielen Arten von Problemen geführt hat, die unten beschrieben werden. Aber schauen wir uns zuerst an, wie die Eingaben und Ausgaben des Layouts aussehen.

Beim Ausführen des Layouts auf einem Knoten in dieser Baumstruktur werden die Elemente "Stil plus DOM" und alle übergeordneten Einschränkungen aus dem übergeordneten Layoutsystem (Raster, Block oder Flex) konzeptionell ausgeführt und der Layouteinschränkungsalgorithmus ausgeführt, um das Ergebnis zu erhalten.

Das zuvor beschriebene Konzeptmodell

Unsere neue Architektur formalisiert dieses konzeptionelle Modell. Wir haben immer noch den Layoutbaum, verwenden ihn aber hauptsächlich, um die Ein- und Ausgaben des Layouts festzuhalten. Für die Ausgabe generieren wir ein völlig neues, unveränderliches Objekt, den sogenannten Fragmentbaum.

Fragmentbaum.

Ich habe bereits den unveränderlichen Fragmentbaum behandelt und beschrieben, wie er dazu dient, große Teile des vorherigen Baums für inkrementelle Layouts wiederzuverwenden.

Außerdem speichern wir das übergeordnete Einschränkungsobjekt, von dem dieses Fragment generiert wurde. Wir verwenden sie als Cache-Schlüssel. Auf diesen werden wir weiter unten noch genauer eingehen.

Auch der Inline-Layout-Algorithmus (Text) wird so umgeschrieben, dass er der neuen unveränderlichen Architektur entspricht. Es erstellt nicht nur die unveränderliche Flat List-Darstellung für das Inline-Layout, sondern bietet auch Caching auf Absatzebene für eine schnellere Neuanordnung, die Form pro Absatz zum Anwenden von Schriftfunktionen auf Elemente und Wörter, einen neuen bidirektionalen Unicode-Algorithmus mit ICU, zahlreiche Fehlerkorrekturen und vieles mehr.

Arten von Layoutfehlern

Layoutfehler lassen sich allgemein in vier Kategorien mit jeweils unterschiedlichen Ursachen unterteilen.

Richtigkeit

Wenn wir über Fehler im Rendering-System nachdenken, denken wir in der Regel an die Richtigkeit, z. B.: „Browser A hat ein Verhalten von X, während Browser B ein Verhalten von Y hat“ oder „Browser A und B sind beide fehlerhaft“. Früher haben wir viel Zeit damit verbracht, und ständig mit dem System zu kämpfen. Häufig wurde eine gezielte Korrektur auf einen Fehler angewendet, aber Wochen später sollten wir feststellen, dass wir in einem anderen, scheinbar nicht relevanten Teil des Systems eine Regression verursacht hatten.

Wie in vorherigen Beiträgen beschrieben, ist dies ein Zeichen für ein sehr anschädigendes System. Insbesondere im Hinblick auf das Layout hatten wir keinen klaren Vertrag zwischen den Klassen, sodass die Browserentwickler von dem Zustand abhängig waren, den sie nicht sollten, oder einen Wert aus einem anderen Teil des Systems falsch interpretieren.

So hatten wir beispielsweise einmal über ein Jahr hinweg eine Kette von etwa 10 Fehlern in Bezug auf das Flex-Layout. Jede Fehlerkorrektur hat entweder zu einem Fehler in der Richtigkeit oder mit der Leistung in einem Teil des Systems geführt, was zu einem weiteren Fehler führte.

Mit LayoutNG ist der Vertrag zwischen allen Komponenten im Layoutsystem klar definiert. Änderungen können nun mit größerer Sicherheit übernommen werden. Wir profitieren auch sehr von dem hervorragenden Projekt Web Platform Tests (WPT), mit dem mehrere Parteien einen Beitrag zu einer gemeinsamen Webtestsuite leisten können.

Wir stellen heute fest, dass eine echte Regression auf dem stabilen Kanal in der Regel keine zugehörigen Tests im WPT-Repository enthält und nicht auf einem Missverständnis der Komponentenverträge zurückzuführen ist. Als Teil unserer Richtlinie zur Fehlerbehebung fügen wir außerdem immer einen neuen WPT-Test hinzu, um sicherzustellen, dass kein Browser noch einmal denselben Fehler machen sollte.

Unzureichende Entwertung

Wenn Sie jemals einen mysteriösen Fehler hatten, bei dem die Änderung der Größe des Browserfensters oder das Umschalten einer CSS-Eigenschaft auf magische Weise dazu führt, dass der Fehler verschwindet, dann sind Sie auf ein Problem aufgrund unzureichender Entwertung gestoßen. Tatsächlich galt ein Teil des änderbaren Baums als sauber, aber aufgrund einer Änderung der übergeordneten Einschränkungen stellte dies nicht die richtige Ausgabe dar.

Dies ist bei den unten beschriebenen Layoutmodi mit zwei Durchgängen (zweimaliges Durchlaufen des Layoutbaums zur Bestimmung des endgültigen Layoutstatus) sehr üblich. Unser Code sah zuvor so aus:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

Eine Fehlerbehebung für diese Art von Fehler wäre in der Regel:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

Eine Lösung für diese Art von Problem führte in der Regel zu einem erheblichen Leistungsabfall (siehe Überentwertung unten) und war sehr umständlich.

Heute haben wir (wie oben beschrieben) ein unveränderliches übergeordnetes Einschränkungsobjekt, das alle Eingaben vom übergeordneten Layout bis zum untergeordneten Element beschreibt. Diese wird mit dem resultierenden unveränderlichen Fragment gespeichert. Aus diesem Grund haben wir einen zentralen Ort, an dem wir diese beiden Eingaben differenzieren, um festzustellen, ob für das untergeordnete Element ein weiterer Layoutpass durchgeführt werden muss. Diese differenzierte Logik ist kompliziert, aber gut strukturiert. Das Debuggen dieser Klasse von Problemen mit unzureichender Entwertung führt in der Regel dazu, dass die beiden Eingaben manuell geprüft und entschieden werden, was sich in der Eingabe geändert hat, sodass eine neue Layoutübergabe erforderlich ist.

Korrekturen an diesem abweichenden Code sind in der Regel einfach und aufgrund der einfachen Erstellung dieser unabhängigen Objekte leicht Einheitentest möglich.

Ein Bild mit fester Breite und prozentualer Breite vergleichen
Bei einem Element mit fester Breite/Höhe spielt es keine Rolle, ob die verfügbare Größe erhöht wird. Bei einer prozentbasierten Breite/Höhe hingegen schon. Die available-size wird im Objekt Parent Constraints dargestellt und führt diese Optimierung als Teil des Differenzalgorithmus durch.

Der abweichende Code im obigen Beispiel lautet:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

Hysterese

Diese Klasse von Programmfehlern ähnelt einer unzureichenden Entwertung. Im Wesentlichen war es in dem vorherigen System unglaublich schwierig, sicherzustellen, dass das Layout idempotent war, d. h., das Layout mit denselben Eingaben noch einmal auszuführen und zu derselben Ausgabe zu führen.

Im folgenden Beispiel wird eine CSS-Eigenschaft einfach zwischen zwei Werten hin und her ausgetauscht. Dies führt jedoch zu einem unendlich wachsenden Rechteck.

Das Video und die Demo zeigen einen Hysteresis-Fehler in Chrome 92 und niedriger. Das Problem wurde in Chrome 93 behoben.

Mit unserem vorherigen veränderlichen Baum war es unglaublich einfach, Fehler wie diesen einzuführen. Wenn der Code den Fehler gemacht hätte, die Größe oder Position eines Objekts zur falschen Zeit oder zum falschen Zeitpunkt zu lesen (wie wir beispielsweise die vorherige Größe oder Position nicht „gelöscht“ haben), würden wir sofort einen leichten Hysteresefehler hinzufügen. Diese Fehler tauchen in der Regel nicht in Tests auf, da sich die meisten Tests auf ein einzelnes Layout und Rendering beziehen. Und noch beunruhigender war, dass wir wussten, dass ein Teil dieser Hysterese erforderlich war, um einige Layout-Modi korrekt zu funktionieren. Es gab zwar Fehler, bei denen wir eine Optimierung zur Entfernung eines Layoutpasses vornehmen mussten, aber ein „Fehler“ eingeführt wurde, da für den Layoutmodus zwei Durchgänge erforderlich waren, um die korrekte Ausgabe zu erhalten.

Baumstruktur mit den im vorherigen Text beschriebenen Problemen
Abhängig von den vorherigen Informationen zum Layout-Ergebnis entstehen nicht idempotente Layouts

Da wir bei LayoutNG explizite Eingabe- und Ausgabedatenstrukturen haben und der Zugriff auf den vorherigen Zustand nicht zulässig ist, haben wir diese Art von Fehler im Layoutsystem weitgehend ausgenommen.

Übermäßige Entwertung und Leistung

Dies ist das direkte Gegenteil der Klasse von Fehlern, die eine unzureichende Entwertung angeht. Wenn wir einen Fehler aufgrund einer unzureichenden Entwertung beheben, lösen wir oft einen Leistungs-Clip aus.

Wir mussten oft schwierige Entscheidungen treffen, um Richtigkeit statt Leistung zu bevorzugen. Im nächsten Abschnitt sehen wir uns genauer an, wie wir diese Arten von Leistungsproblemen behoben haben.

Zunahme von Layouts mit zwei Durchgängen und Performance Cliffs

Das Flex- und Raster-Layout stellte eine Veränderung in der Ausdruckskraft von Layouts im Web dar. Diese Algorithmen unterscheiden sich jedoch grundlegend vom vorherigen Block-Layout-Algorithmus.

Bei einem Block-Layout muss die Suchmaschine in fast allen Fällen das Layout für alle untergeordneten Elemente nur genau einmal ausführen. Dies ist hervorragend für die Leistung geeignet, ist aber am Ende nicht so ausdrucksstark, wie es Webentwickler möchten.

Beispielsweise wird oft die Größe aller untergeordneten Elemente auf die größte Größe erweitert. Um dies zu unterstützen, führt das übergeordnete Layout (Flex oder Raster) eine Messkarte aus, um zu bestimmen, wie groß jedes der untergeordneten Elemente ist. Anschließend wird eine Layoutübergabe durchgeführt, um alle untergeordneten Elemente auf diese Größe zu erweitern. Dieses Verhalten ist die Standardeinstellung für das flexible und das Rasterlayout.

Zwei Sätze von Boxen. Das erste zeigt die intrinsische Größe der Boxen im Maßpass, das zweite in gleicher Höhe.

Die Leistung dieser Layouts mit zwei Durchgängen war anfangs akzeptabel, weil sie meist nicht zu eng verschachtelt wurden. Wir stellten jedoch erhebliche Leistungsprobleme fest, da immer komplexere Inhalte aufkamen. Wenn Sie das Ergebnis der Messungsphase nicht im Cache speichern, rastet die Layoutstruktur zwischen dem Status measure und dem endgültigen Status layout hin und her.

Die in der Bildunterschrift erläuterten Layouts mit einem, zwei und drei Durchgängen.
In der Abbildung oben gibt es drei <div>-Elemente. Ein einfaches Layout mit nur einem Durchlauf (wie Blocklayout) verwendet drei Layoutknoten (Komplexitäts-O(n)). Bei einem Layout mit zwei Durchgängen (z. B. Flex oder Raster) kann dies jedoch in diesem Beispiel zu einer Komplexität von O(2n)-Besuchen führen.
Diagramm mit der exponentiellen Zunahme der Layoutzeit.
Dieses Bild und die Demo zeigen ein exponentielles Layout mit Rasterlayout. Dieser Fehler wurde in Chrome 93 durch das Verschieben von Grid in die neue Architektur behoben.

Früher haben wir versucht, dem Flex- und Rasterlayout sehr spezifische Caches hinzuzufügen, um diese Art von Leistungs-Clip zu vermeiden. Das funktionierte (und wir sind mit Flex sehr weit gekommen), hatte aber ständig mit Fehlern im Zusammenhang mit der Entwertung gekämpft.

Mit LayoutNG können explizite Datenstrukturen sowohl für die Eingabe als auch für die Ausgabe eines Layouts erstellt werden. Darüber hinaus haben wir Caches der Messung und Layout-Durchgänge erstellt. Dadurch wird die Komplexität wieder auf O(n) zurückgeführt, was zu einer vorhersehbaren linearen Leistung für Webentwickler führt. Sollte ein Layout ein Layout mit drei Durchgängen haben, wird einfach auch diese Karte bzw. dieses Ticket im Cache gespeichert. Dies kann in Zukunft möglicherweise die Möglichkeit bieten, erweiterte Layoutmodi sicher einzuführen. Ein Beispiel dafür, wie RenderingNG die Erweiterbarkeit in allen Bereichen grundlegend ermöglicht. In einigen Fällen kann für das Rasterlayout ein Layout mit drei Durchgängen erforderlich sein, derzeit aber nur sehr selten.

Wenn Entwickler Leistungsprobleme speziell mit dem Layout feststellen, liegt dies normalerweise an einem Fehler in der exponentiellen Layoutzeit und nicht auf dem Rohdurchsatz der Layoutphase der Pipeline. Wenn eine kleine inkrementelle Änderung (ein Element, das eine einzelne CSS-Eigenschaft ändert) zu einem Layout von 50 bis 100 ms führt, handelt es sich wahrscheinlich um einen exponentiellen Layoutfehler.

Zusammenfassung

Das Layout ist ein extrem komplexer Bereich und wir haben nicht alle interessanten Details wie Optimierungen wie Inline-Layoutoptimierungen behandelt (wie das gesamte Inline- und Text-Subsystem funktioniert). Selbst die hier besprochenen Konzepte kratzten wirklich nur an der Oberfläche und glänzten dabei mit vielen Details. Aber hoffentlich haben wir gezeigt, wie die systematische Verbesserung der Architektur eines Systems langfristig zu enormen Vorteilen führen kann.

Uns ist aber auch klar, dass noch viel Arbeit vor uns liegt. Wir arbeiten an einer Reihe von Problemen (sowohl Leistung als auch Richtigkeit), an deren Lösung wir arbeiten, und wir freuen uns über neue Layoutfunktionen für CSS. Wir sind davon überzeugt, dass die Architektur von LayoutNG dafür sorgt, dass die Lösung dieser Probleme sicher und umsetzbar ist.

Ein Bild (du weißt, welches!) von Una Kravets