RenderingNG im Detail: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Ich bin Ian Kilpatrick, Engineering Lead im Blink-Layoutteam und Koji Ishii. Bevor wir im Blink-Team arbeiten, Ich war Frontend-Engineer (bevor Google die Rolle des Frontend-Engineers hatte) in Google Docs, Drive und Gmail entwickelt. Nach etwa fünf Jahren in dieser Position wechselte ich ins Blink-Team, bei der Arbeit effektiv C++ lernen, und versuchten, die extrem komplexe Blink-Codebasis zu erweitern. Selbst heute verstehe ich nur einen kleinen Teil davon. Ich bin dankbar für die Zeit, die ich mir in dieser Zeit gegeben habe. Ich fühlte mich gut darin, dass viele "Frontend-Entwickler" wiedergefunden werden. wurde zum "Browserentwickler" übergehen. vor mir.

Meine bisherigen Erfahrungen haben mich bei der Arbeit im Blink-Team persönlich geprägt. Als Frontend-Entwickler bin ich ständig auf Browserinkonsistenzen gestoßen, Leistungsprobleme, Renderingfehler und fehlende Funktionen. LayoutNG bot mir die Gelegenheit, diese Probleme systematisch im Layoutsystem von Blink zu beheben. und repräsentiert die Summe der Aufgaben gearbeitet haben.

In diesem Beitrag erkläre ich, wie eine solche umfangreiche Architekturänderung verschiedene Arten von Fehlern und Leistungsproblemen reduzieren und minimieren kann.

Layout-Engine-Architekturen aus 9.000 m Höhe

Früher habe ich den Layoutbaum von Blink als „veränderliche Baumstruktur“ bezeichnet.

Zeigt den Baum wie im folgenden Text beschrieben.

Jedes Objekt in der Layoutstruktur enthielt input-Informationen. etwa die von einem übergeordneten Element vorgegebene Größe, die Position von Gleitkommazahlen und Ausgabeinformationen zum Beispiel die endgültige Breite und Höhe des Objekts oder seine X- und Y-Position.

Diese Objekte wurden zwischen den Renderings beibehalten. Bei einer Stiländerung haben wir dieses Objekt als „schmutzig“ markiert und ebenso alle übergeordneten Elemente im Baum. Nach der Layoutphase der Rendering-Pipeline Dann haben wir den Baum gereinigt, alle schmutzigen Objekte spaziert und dann das Layout ausgeführt, um sie in einen sauberen Zustand zu bringen.

Wir haben festgestellt, dass diese Architektur zu vielen verschiedenen Arten von Problemen führte. die nachfolgend beschrieben werden. Aber gehen wir zuerst einen Schritt zurück und überlegen, was die Ein- und Ausgaben des Layouts sind.

Beim Ausführen des Layouts auf einem Knoten in diesem Baum wird konzeptionell die Kombination aus Stil und DOM sowie alle übergeordneten Einschränkungen aus dem übergeordneten Layoutsystem (Raster, Block oder Flex) führt den Algorithmus für die Layouteinschränkung aus und liefert ein Ergebnis.

Das zuvor beschriebene konzeptionelle Modell.

Unsere neue Architektur formalisiert dieses konzeptionelle Modell. Wir haben noch den Layoutbaum, aber wir verwenden ihn in erster Linie, um die Ein- und Ausgaben des Layouts zu speichern. Für die Ausgabe generieren wir ein vollständig neues, immutable Objekt, das als immutable bezeichnet wird.

Der Fragmentbaum.

Ich habe die Themen zuvor unveränderlichen Fragmentbaum beschreiben, wie große Teile des vorherigen Baums für inkrementelle Layouts wiederverwendet werden sollen.

Außerdem speichern wir das übergeordnete Einschränkungsobjekt, das dieses Fragment generiert hat. Wir verwenden ihn als Cache-Schlüssel, was im Folgenden näher erläutert wird.

Der Inline-Layoutalgorithmus (Text) wird ebenfalls umgeschrieben, um der neuen unveränderlichen Architektur zu entsprechen. So entsteht nicht nur die Unveränderliche einfache Listendarstellung für das Inline-Layout, bietet aber auch Caching auf Absatzebene für ein schnelleres Layout, Schriftart-pro-Absatz an, um Schriftmerkmale auf Elemente und Wörter anzuwenden, einen neuen bidirektionalen Unicode-Algorithmus mit der ITS, viele Fehlerkorrekturen und mehr.

Arten von Layoutfehlern

Layoutfehler lassen sich im Prinzip in vier verschiedene Kategorien einteilen: mit unterschiedlichen Grundursachen.

Richtigkeit

Bei Fehlern im Renderingsystem geht es meist um Richtigkeit, Beispiel: „Browser A verhält sich X, Browser B verhält sich Y“, oder „Browser A und B funktionieren nicht“. Zuvor haben wir viel Zeit damit verbracht, Dabei kämpften wir ständig mit dem System. Ein häufiger Fehlermodus war die gezielte Korrektur eines Fehlers. Aber stellen Sie nach Wochen später fest, dass wir eine Regression in einem anderen, scheinbar nicht zusammenhängenden Teil des Systems verursacht haben.

Wie in früheren Beiträgen beschrieben, ist das ein Zeichen für ein sehr instabiles System. Speziell für das Layout gab es keinen sauberen Vertrag zwischen den Klassen, sodass Browserentwickler sich auf einen Zustand verlassen, oder einen Wert aus einem anderen System falsch interpretieren.

Ein Beispiel: Wir hatten eine Kette von etwa 10 Bugs über einen Zeitraum von mehr als einem Jahr. mit dem flexiblen Layout. Jede Korrektur führte entweder zu einem Fehler- oder Leistungsproblem in einem Teil des Systems, was zu einem weiteren Fehler führt.

Nun, da LayoutNG den Vertrag zwischen allen Komponenten im Layoutsystem klar definiert, haben wir festgestellt, dass wir Änderungen mit größerer Sicherheit anwenden können. Außerdem profitieren wir stark vom Projekt Web Platform Tests (WPT), womit mehrere Parteien an einer gemeinsamen Webtest-Suite beitragen können.

Wenn wir eine echte Regression auf unserem stabilen Kanal veröffentlichen, Normalerweise sind keine Tests im WPT-Repository enthalten. und nicht auf Missverständnissen von Komponentenverträgen zurückzuführen ist. Außerdem fügen wir im Rahmen unserer Richtlinie zur Fehlerbehebung immer einen neuen WPT-Test hinzu. So wird sichergestellt, dass kein Browser denselben Fehler wiederholt.

Unterentwertung

Wenn Sie schon einmal einen mysteriösen Fehler hatten, bei dem der Fehler durch die Größenanpassung des Browserfensters oder durch Umschalten einer CSS-Eigenschaft verschwindet, sind Sie auf ein Problem mit Unterentwertung gestoßen. Effektiv galt ein Teil des veränderlichen Baums als sauber, aber aufgrund von Änderungen bei den Einschränkungen für übergeordnete Elemente wurde die Ausgabe nicht korrekt dargestellt.

Dies ist sehr üblich bei der Verwendung von zwei Durchgängen (zweimaliges Durchlaufen des Layoutbaums, um den endgültigen Layout-Status zu bestimmen) unten beschrieben. Bisher sah unser Code wie folgt aus:

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

Eine Fehlerbehebung für diese Art von Fehler sieht in der Regel so aus:

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

Eine Lösung dieser Art von Problem führt in der Regel zu einem erheblichen Leistungsabfall. (siehe übermäßige Entwertung unten) und die Behebung sehr schwierig.

Heute haben wir (wie oben beschrieben) ein unveränderliches übergeordnetes Einschränkungsobjekt, das alle Eingaben vom übergeordneten Layout in das untergeordnete Element beschreibt. Wir speichern diese zusammen mit dem resultierenden unveränderlichen Fragment. Aus diesem Grund Wir haben einen zentralen Ort, an dem wir diese beiden Eingaben unterscheiden, um festzustellen, ob für das untergeordnete Element ein weiterer Layoutübergang ausgeführt werden muss. Diese Differenzlogik ist kompliziert, aber gut strukturiert. Das Beheben dieser Klasse von Problemen mit Unterentwertung führt normalerweise dazu, dass die beiden Eingaben manuell geprüft werden und entscheiden, was sich geändert hat, sodass ein weiterer Layout-Pass erforderlich ist.

Die Fehlerbehebungen für diesen abweichenden Code sind in der Regel einfach. und aufgrund der einfachen Erstellung dieser unabhängigen Objekte leicht Einheitentestbar machen.

<ph type="x-smartling-placeholder">
</ph> Bilder mit fester und prozentualer Breite im Vergleich <ph type="x-smartling-placeholder">
</ph> Bei einem Element mit fester Breite/Höhe spielt es keine Rolle, ob die verfügbare Größe zunimmt, eine prozentbasierte Breite/Höhe hingegen schon. Die available-size wird im Objekt Parent Constraints dargestellt und wird als Teil des Differenzalgorithmus diese Optimierung durchführen.

Der abweichende Code für das obige 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 Fehlern ähnelt der Untervalidierung. Im bisherigen System war es unglaublich schwierig, sicherzustellen, dass das Layout idempotent war, das Layout erneut mit denselben Eingaben ausgeführt wurde. Das führte zur selben Ausgabe.

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

<ph type="x-smartling-placeholder">
</ph>
Im Video und in der Demo wird in Chrome 92 und niedriger ein Hysteresis-Fehler angezeigt. In Chrome 93 wurde das Problem behoben.

Mit unserem vorherigen änderbaren Baum war es unglaublich einfach, solche Fehler einzuführen. Wenn der Code den Fehler gemacht hat, die Größe oder Position eines Objekts zur falschen Zeit oder in der falschen Phase zu lesen (da wir beispielsweise die vorherige Größe oder Position nicht „gelöscht“ haben) würden wir sofort einen subtilen Hysterese-Fehler hinzufügen. Diese Fehler treten in der Regel bei Tests nicht auf, da sich die meisten Tests auf ein einzelnes Layout und Rendering beziehen. Noch besorgniserregend war, dass wir wussten, dass ein Teil dieser Hysterese erforderlich ist, damit einige Layoutmodi richtig funktionierten. Es gab Programmfehler, bei denen wir eine Optimierung zum Entfernen einer Layoutübergabe durchführten, aber es wird ein „Fehler“ da für den Layoutmodus zwei Durchgänge erforderlich waren, um die richtige Ausgabe zu erhalten.

<ph type="x-smartling-placeholder">
</ph> Eine Baumstruktur, die die im vorherigen Text beschriebenen Probleme veranschaulicht. <ph type="x-smartling-placeholder">
</ph> Abhängig von den vorherigen Layout-Ergebnisinformationen führt dies zu nicht idempotenten Layouts.

Bei LayoutNG, da wir explizite Eingabe- und Ausgabedatenstrukturen haben, und der Zugriff auf den vorherigen Status nicht erlaubt ist, haben wir diese Klasse von Fehlern aus dem Layoutsystem entfernt.

Übermäßige Entwertung und Leistung

Dies ist das direkte Gegenteil der Fehlerklasse der Untervalidierung. Häufig lösten wir bei der Behebung eines Untervalidierungsfehlers eine Leistungsklippe aus.

Wir hatten oft schwierige Entscheidungen zu treffen, bevor wir Korrektheit Vorrang vor Leistung gaben. Im nächsten Abschnitt sehen wir uns genauer an, wie wir diese Arten von Leistungsproblemen behoben haben.

Anstieg der 2 Pässe und Performance Cliffs

Das Flex- und Raster-Layout stellte einen Wandel in der Ausdruckskraft von Layouts im Web dar. Diese Algorithmen unterscheiden sich jedoch grundlegend von dem Block-Layout-Algorithmus, der vorher eingesetzt wurde.

Für das Block-Layout ist in fast allen Fällen erforderlich, dass die Suchmaschine das Layout für alle untergeordneten Elemente nur genau einmal ausführt. Dadurch wird die Leistung verbessert, aber letztendlich ist es nicht so ausdrucksstark, wie Webentwickler es wünschen.

Beispiel: wird die Größe aller untergeordneten Elemente auf die Größe der größten erweitert. Um dies zu unterstützen, wird das übergeordnete Layout (Flex- oder Raster-Layout) ermittelt, wie groß jedes der untergeordneten Elemente ist, dann ein Layout-Pass, um alle untergeordneten Elemente auf diese Größe zu erweitern. Dieses Verhalten ist sowohl für das flexible als auch das Rasterlayout die Standardeinstellung.

Zwei Sätze von Boxen. Der erste ist die unveränderliche Größe der Boxen in der Messungskarte, der zweite im Layout alle die gleiche Höhe.

Diese Layouts mit zwei Durchgängen waren anfangs in Bezug auf die Leistung akzeptabel, da die Menschen sie normalerweise nicht zu sehr verschachteln. Mit zunehmend komplexeren Inhalten stellten wir jedoch erhebliche Leistungsprobleme fest. Wenn Sie das Ergebnis der Messungsphase nicht im Cache speichern, Die Layoutstruktur wechselt zwischen dem Zustand measure und dem endgültigen layout-Status.

<ph type="x-smartling-placeholder">
</ph> Das Layout mit einem, zwei und drei Durchgängen, das im Untertitel erklärt wird. <ph type="x-smartling-placeholder">
</ph> Im obigen Bild haben wir drei <div>-Elemente. Bei einem einfachen Layout mit einem Durchlauf (wie einem Blocklayout) werden drei Layoutknoten verwendet (Komplexität O(n)). Bei einem Layout mit zwei Durchläufen (wie Flex oder Raster) kann dies in diesem Beispiel zu einer Komplexität von O(2n)-Besuchen führen.
<ph type="x-smartling-placeholder">
</ph> Diagramm, das die exponentielle Zunahme der Layoutzeit zeigt. <ph type="x-smartling-placeholder">
</ph> Dieses Bild und die Demo zeigen ein exponentielles Layout mit Rasterlayout. Dies wurde in Chrome 93 durch die Umstellung von Grid auf die neue Architektur behoben.

Früher haben wir versucht, dem Flex- und Raster-Layout sehr spezifische Caches hinzuzufügen, um diese Art von Leistungsklippen zu beseitigen. Das hat funktioniert (und mit Flex sind wir sehr weit gekommen), kämpften aber ständig mit zu wenig und zu vielen Fehlern in der Entwertung.

Mit LayoutNG können wir explizite Datenstrukturen für die Ein- und Ausgabe von Layout, Außerdem haben wir Caches für Messungs- und Layoutkarten erstellt. Dadurch wird die Komplexität wieder auf O(n), was Webentwicklern eine vorhersehbare lineare Leistung zur Folge hat. Sollte ein Layout einmal ein Layout mit drei Durchläufen ausführen, speichern wir diese Passagen einfach im Cache. Dies könnte in der Zukunft die Möglichkeit eröffnen, erweiterte Layoutmodi sicher einzuführen – ein Beispiel dafür, wie das Rendern von Erweiterbarkeit in allen Bereichen. In einigen Fällen können für das Rasterlayout Layouts mit drei Durchläufen erforderlich sein, was derzeit jedoch äußerst selten ist.

Wenn Entwickelnde mit Leistungsproblemen konfrontiert sind, Dies liegt normalerweise an einem Fehler in der exponentiellen Layoutzeit und nicht an dem Rohdurchsatz der Layoutphase der Pipeline. Wenn eine kleine schrittweise Änderung (ein Element ändert eine einzelne CSS-Eigenschaft) zu einem Layout von 50 bis 100 ms führt, ist das wahrscheinlich ein exponentieller Layoutfehler.

Zusammenfassung

Das Layout ist ein sehr komplexer Bereich, und wir haben nicht alle möglichen interessanten Details behandelt, wie z. B. Inline-Layoutoptimierungen. (die Funktionsweise des gesamten Inline- und Text-Subsystems), und selbst die hier besprochenen Konzepte kratzten nur an der Oberfläche, und viele Details behandelt. Wir haben jedoch hoffentlich gezeigt, wie die systematische Verbesserung der Architektur eines Systems langfristig zu enormen Verbesserungen führen kann.

Dennoch wissen wir, dass noch eine Menge Arbeit vor uns liegt. Wir kennen verschiedene Arten von Problemen (sowohl Leistung als auch Richtigkeit), an deren Lösung wir arbeiten. und freuen uns auf neue Layoutfunktionen für CSS. Wir sind davon überzeugt, dass die Architektur von LayoutNG sicher und umsetzbar ist.

Ein Bild von Una Kravets