Ich bin Ian Kilpatrick, Engineering Lead im Blink-Layoutteam, zusammen mit 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, 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.
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.
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, unveränderliches Objekt, das als Fragmentstruktur bezeichnet wird.
Ich habe die Themen 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 werden nicht nur die Unveränderliche einfache Listendarstellung für das Inline-Layout, bietet aber auch Caching auf Absatzebene für ein schnelleres Layout, „Form pro Absatz“, um Schriftfunktionen auf Elemente und Wörter anzuwenden, einen neuen bidirektionalen Unicode-Algorithmus mit der ITS, viele Korrekturen 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 gibt es keine zugehörigen Tests im WPT-Repository. 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 die Größe des Browserfensters oder das Umschalten einer CSS-Eigenschaft automatisch nicht mehr angezeigt wird, sind Sie auf ein Problem mit Unterentwertung gestoßen. Effektiv galt ein Teil des veränderlichen Baums als sauber, Aufgrund von Änderungen bei den übergeordneten Einschränkungen zeigte sie jedoch nicht die richtige Ausgabe.
Dies ist sehr üblich bei der Verwendung von (zweimaliges Durchlaufen des Layoutbaums, um den endgültigen Layout-Status zu bestimmen) unten beschrieben. Bisher sah der Code so 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-Pass ausgeführt werden muss. Diese Differenzlogik ist kompliziert, aber gut strukturiert. Das Debugging 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">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">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 korrekt funktionieren. 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">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. Bei der Behebung eines Fehlers bei der Unterentwertung lösten wir häufig eine Leistungsklippe aus.
Wir mussten oft schwierige Entscheidungen treffen, bevor wir Richtigkeit statt Leistung bevorzugt haben. 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.
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 type="x-smartling-placeholder">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