RenderingNG diepgaande analyse: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Ik ben Ian Kilpatrick, technisch leider van het Blink-lay-outteam, samen met Koji Ishii. Voordat ik bij het Blink-team werkte, was ik front-end engineer (voordat Google de rol van 'front-end engineer' had), waarbij ik functies bouwde binnen Google Documenten, Drive en Gmail. Na ongeveer vijf jaar in die rol waagde ik de grote gok door over te stappen naar het Blink-team, waarbij ik effectief C++ on the job leerde en probeerde de enorm complexe Blink-codebase verder uit te bouwen. Zelfs vandaag de dag begrijp ik er maar een relatief klein deel van. Ik ben dankbaar voor de tijd die ik in deze periode heb gekregen. Ik werd getroost door het feit dat veel "herstellende front-end engineers" vóór mij de overstap naar een "browser engineer" maakten.

Mijn eerdere ervaringen hebben mij persoonlijk begeleid toen ik bij het Blink-team zat. Als front-end engineer kwam ik voortdurend browser-inconsistenties, prestatieproblemen, weergavefouten en ontbrekende functies tegen. LayoutNG was voor mij een kans om deze problemen systematisch op te lossen binnen het lay-outsysteem van Blink, en vertegenwoordigt de som van de inspanningen van veel ingenieurs door de jaren heen.

In dit bericht leg ik uit hoe een grote architectuurverandering als deze verschillende soorten bugs en prestatieproblemen kan verminderen en verzachten.

Een overzicht van 9.000 meter van de layout-engine-architecturen

Voorheen was de lay-outboom van Blink wat ik een "veranderlijke boom" zal noemen.

Toont de boom zoals beschreven in de volgende tekst.

Elk object in de lay-outboom bevatte invoerinformatie , zoals de beschikbare grootte opgelegd door een ouder, de positie van eventuele drijvers, en uitvoerinformatie , bijvoorbeeld de uiteindelijke breedte en hoogte van het object of de x- en y-positie ervan.

Deze objecten werden tussen de renders bewaard. Toen er een stijlverandering plaatsvond, markeerden we dat object als vies en eveneens alle ouders in de boom. Toen de lay-outfase van de renderingpijplijn werd uitgevoerd, maakten we vervolgens de boom schoon, lieten we alle vuile objecten lopen en voerden we vervolgens de lay-out uit om ze in een schone staat te krijgen.

We hebben ontdekt dat deze architectuur tot veel soorten problemen heeft geleid, die we hieronder zullen beschrijven. Maar laten we eerst een stap terug doen en bekijken wat de in- en uitgangen van de lay-out zijn.

Het uitvoeren van een lay-out op een knooppunt in deze boom neemt conceptueel de "Stijl plus DOM" en alle bovenliggende beperkingen van het bovenliggende lay-outsysteem (raster, blok of flex) in beslag, voert het algoritme voor de lay-outbeperking uit en levert een resultaat op.

Het conceptuele model dat eerder is beschreven.

Onze nieuwe architectuur formaliseert dit conceptuele model. We hebben nog steeds de lay-outboom, maar gebruiken deze voornamelijk om de in- en uitgangen van de lay-out vast te houden. Voor de uitvoer genereren we een volledig nieuw, onveranderlijk object, de fragmentboom genaamd.

De fragmentboom.

Ik heb eerder de onveranderlijke fragmentboom besproken en beschreven hoe deze is ontworpen om grote delen van de vorige boom te hergebruiken voor incrementele lay-outs.

Bovendien slaan we het bovenliggende beperkingsobject op dat dat fragment heeft gegenereerd. We gebruiken dit als een cachesleutel die we hieronder verder zullen bespreken.

Het inline (tekst) lay-outalgoritme is ook herschreven om te passen bij de nieuwe onveranderlijke architectuur. Het produceert niet alleen de onveranderlijke platte lijstweergave voor inline-indeling, maar beschikt ook over caching op paragraafniveau voor snellere relay-out, vorm-per-paragraaf om lettertype-eigenschappen toe te passen op elementen en woorden, een nieuw Unicode bidirectioneel algoritme dat ICU gebruikt, veel correctheid reparaties en meer.

Soorten lay-outfouten

Layoutbugs vallen grofweg in vier verschillende categorieën, elk met verschillende hoofdoorzaken.

Juistheid

Als we nadenken over bugs in het weergavesysteem, denken we doorgaans aan correctheid, bijvoorbeeld: "Browser A vertoont X-gedrag, terwijl Browser B Y-gedrag vertoont", of "Browsers A en B zijn beide kapot". Voorheen besteedden we hier veel tijd aan, en daarbij waren we voortdurend in gevecht met het systeem. Een veel voorkomende fout was het toepassen van een zeer gerichte oplossing voor één bug, maar weken later ontdekten we dat we een regressie hadden veroorzaakt in een ander (schijnbaar niet-gerelateerd) deel van het systeem.

Zoals beschreven in eerdere berichten is dit een teken van een zeer broos systeem. Specifiek voor de lay-out hadden we geen duidelijk contract tussen de klassen, waardoor browseringenieurs afhankelijk waren van de status die ze niet zouden moeten hebben, of een bepaalde waarde uit een ander deel van het systeem verkeerd interpreteerden.

Op een gegeven moment hadden we bijvoorbeeld in de loop van meer dan een jaar een reeks van ongeveer tien bugs die verband hielden met de flex-indeling. Elke oplossing veroorzaakte een correctheids- of prestatieprobleem in een deel van het systeem, wat leidde tot weer een nieuwe bug.

Nu LayoutNG het contract tussen alle componenten in het lay-outsysteem duidelijk definieert, hebben we ontdekt dat we veranderingen met veel meer vertrouwen kunnen toepassen. Ook hebben wij veel profijt van het uitstekende project Web Platform Tests (WPT), waarmee meerdere partijen kunnen bijdragen aan een gemeenschappelijke webtestsuite.

Tegenwoordig ontdekken we dat als we een echte regressie op ons stabiele kanaal vrijgeven, deze doorgaans geen bijbehorende tests in de WPT-repository heeft en niet het gevolg is van een verkeerd begrip van componentcontracten. Verder voegen we, als onderdeel van ons bugfix-beleid, altijd een nieuwe WPT-test toe, om ervoor te zorgen dat geen enkele browser opnieuw dezelfde fout maakt.

Onder-invalidatie

Als u ooit een mysterieuze bug heeft gehad waarbij het wijzigen van de grootte van het browservenster of het wisselen van een CSS-eigenschap de bug op magische wijze doet verdwijnen, dan bent u een probleem van te weinig validering tegengekomen. In feite werd een deel van de veranderlijke boom als schoon beschouwd, maar vanwege een wijziging in de bovenliggende beperkingen vertegenwoordigde dit niet de juiste uitvoer.

Dit is heel gebruikelijk bij de hieronder beschreven lay-outmodi met twee doorgangen (twee keer door de lay-outboom lopen om de uiteindelijke lay-outstatus te bepalen). Voorheen zag onze code er als volgt uit:

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

Een oplossing voor dit type bug zou doorgaans het volgende zijn:

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

Een oplossing voor dit type probleem zou normaal gesproken een ernstige achteruitgang van de prestaties veroorzaken (zie over-invalidatie hieronder), en het was erg lastig om dit correct te krijgen.

Tegenwoordig hebben we (zoals hierboven beschreven) een onveranderbaar ouderbeperkingsobject dat alle invoer van de bovenliggende lay-out naar het kind beschrijft. We slaan dit op met het resulterende onveranderlijke fragment. Hierdoor hebben we een gecentraliseerde plek waar we deze twee inputs van elkaar onderscheiden om te bepalen of het kind nog een lay-outpas moet laten uitvoeren. Deze uiteenlopende logica is ingewikkeld, maar goed ingeperkt. Het opsporen van fouten in deze klasse van problemen met onvoldoende validering resulteert doorgaans in het handmatig inspecteren van de twee ingangen en het beslissen wat er in de ingang is veranderd, zodat een nieuwe lay-outpas vereist is.

Oplossingen voor deze afwijkende code zijn doorgaans eenvoudig en gemakkelijk unit-testbaar vanwege de eenvoud van het maken van deze onafhankelijke objecten.

Een afbeelding met een vaste breedte en een percentage breedte vergelijken.
Voor een element met een vaste breedte/hoogte maakt het niet uit of de beschikbare grootte groter wordt, maar een op een percentage gebaseerde breedte/hoogte wel. De beschikbare grootte wordt weergegeven in het Parent Constraints- object en zal als onderdeel van het diffing-algoritme deze optimalisatie uitvoeren.

De afwijkende code voor het bovenstaande voorbeeld is:

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

Deze klasse bugs is vergelijkbaar met onderinvalidatie. In wezen was het in het vorige systeem ongelooflijk moeilijk om ervoor te zorgen dat de lay-out idempotent was, dat wil zeggen: het opnieuw uitvoeren van de lay-out met dezelfde invoer resulteerde in dezelfde uitvoer.

In het onderstaande voorbeeld schakelen we eenvoudigweg een CSS-eigenschap heen en weer tussen twee waarden. Dit resulteert echter in een "oneindig groeiende" rechthoek.

De video en demo tonen een hysteresisbug in Chrome 92 en lager. Het is opgelost in Chrome 93.

Met onze vorige veranderlijke stamboom was het ongelooflijk eenvoudig om dit soort bugs te introduceren. Als de code de fout zou maken om de grootte of positie van een object op het verkeerde tijdstip of in het verkeerde stadium te lezen (omdat we bijvoorbeeld de vorige grootte of positie niet "wissen"), zouden we onmiddellijk een subtiele hysteresisbug toevoegen. Deze bugs komen doorgaans niet voor tijdens het testen, omdat de meeste tests zich richten op een enkele lay-out en weergave. Nog zorgwekkender was dat we wisten dat een deel van deze hysteresis nodig was om sommige lay-outmodi correct te laten werken. We hadden bugs waarbij we een optimalisatie uitvoerden om een ​​lay-outpas te verwijderen, maar we introduceerden een "bug" omdat de lay-outmodus twee passages nodig had om de juiste uitvoer te krijgen.

Een boom die de problemen demonstreert die in de voorgaande tekst zijn beschreven.
Afhankelijk van de eerdere lay-outresultaatinformatie resulteert dit in niet-idempotente lay-outs

Met LayoutNG hebben we, omdat we expliciete invoer- en uitvoerdatastructuren hebben en toegang tot de vorige status niet is toegestaan, deze klasse bugs in het lay-outsysteem grotendeels verholpen.

Over-invalidatie en prestaties

Dit is het directe tegenovergestelde van de klasse van bugs die te weinig valideren. Vaak veroorzaakten we bij het oplossen van een bug met te weinig validering een prestatieklif.

We moesten vaak moeilijke keuzes maken waarbij we juistheid verkozen boven prestatie. In het volgende gedeelte gaan we dieper in op de manier waarop we dit soort prestatieproblemen hebben verholpen.

Opkomst van de lay-outs met twee passen en prestatiekliffen

Flex- en rasterlay-out vertegenwoordigden een verschuiving in de expressiviteit van lay-outs op internet. Deze algoritmen waren echter fundamenteel verschillend van het bloklay-outalgoritme dat eraan voorafging.

Blokindeling vereist (in bijna alle gevallen) dat de engine slechts één keer de indeling van al zijn kinderen uitvoert. Dit is geweldig voor de prestaties, maar is uiteindelijk niet zo expressief als webontwikkelaars willen.

Vaak wil je bijvoorbeeld dat de maat van alle kinderen groter wordt naar de maat van de grootste. Om dit te ondersteunen zal de bovenliggende lay-out (flex of grid) een meting uitvoeren om te bepalen hoe groot elk van de kinderen is, en vervolgens een lay-out uitvoeren om alle kinderen tot deze maat uit te rekken. Dit gedrag is de standaard voor zowel de flex- als de rasterindeling.

Twee sets dozen, de eerste toont de intrinsieke grootte van de dozen in de maatpas, de tweede bij indeling allemaal even hoog.

Deze lay-outs met twee doorgangen waren aanvankelijk qua prestaties acceptabel, omdat mensen ze doorgaans niet diep nestelden. We begonnen echter aanzienlijke prestatieproblemen te zien naarmate er complexere inhoud ontstond. Als u het resultaat van de meetfase niet in de cache opslaat, zal de lay-outboom heen en weer bewegen tussen de meetstatus en de uiteindelijke lay- outstatus.

De lay-outs met één, twee en drie passen worden uitgelegd in het bijschrift.
In de bovenstaande afbeelding hebben we drie <div> -elementen. Een eenvoudige lay-out met één doorgang (zoals een bloklay-out) bezoekt drie lay-outknooppunten (complexiteit O(n)). Voor een lay-out met twee doorgangen (zoals flex of grid) kan dit echter potentieel resulteren in complexiteit van O( 2n )-bezoeken voor dit voorbeeld.
Grafiek die de exponentiële toename van de lay-outtijd laat zien.
Deze afbeelding en demo tonen een exponentiële lay-out met rasterlay-out. Dit is opgelost in Chrome 93 als gevolg van het verplaatsen van Grid naar de nieuwe architectuur

Voorheen probeerden we heel specifieke caches toe te voegen aan de flex- en grid-indeling om dit soort prestatiekliffen tegen te gaan. Dit werkte (en we kwamen heel ver met Flex), maar we hadden voortdurend te kampen met onder- en over-invalidatiebugs.

Met LayoutNG kunnen we expliciete datastructuren creëren voor zowel de invoer als de uitvoer van de lay-out, en bovendien hebben we caches gebouwd van de maat- en lay-outpassen. Dit brengt de complexiteit terug naar O(n), wat resulteert in voorspelbaar lineaire prestaties voor webontwikkelaars. Als er ooit een geval is waarin een lay-out een lay-out met drie doorgangen uitvoert, zullen we die doorgang eenvoudigweg ook in de cache opslaan. Dit kan mogelijkheden bieden om in de toekomst veilig meer geavanceerde lay-outmodi te introduceren - een voorbeeld van hoe RenderingNG uitbreidbaarheid over de hele linie fundamenteel ontgrendelt . In sommige gevallen kan een rasterlay-out een lay-out met drie doorgangen vereisen, maar dit is momenteel uiterst zeldzaam.

We ontdekken dat wanneer ontwikkelaars prestatieproblemen tegenkomen, specifiek met de lay-out, dit doorgaans te wijten is aan een exponentiële lay-outtijdfout en niet aan de ruwe doorvoer van de lay-outfase van de pijplijn. Als een kleine stapsgewijze verandering (één element dat een enkele CSS-eigenschap verandert) resulteert in een lay-out van 50-100 ms, is dit waarschijnlijk een exponentiële lay-outfout.

Samengevat

Lay-out is een enorm complex gebied, en we hebben niet allerlei interessante details behandeld, zoals inline-lay-outoptimalisaties (hoe het hele inline- en tekst-subsysteem eigenlijk werkt), en zelfs de hier besproken concepten bespraken eigenlijk alleen maar het oppervlak. en veel details verdoezeld. Hopelijk hebben we echter laten zien hoe het systematisch verbeteren van de architectuur van een systeem op de lange termijn tot buitensporige winsten kan leiden.

Dat gezegd hebbende, weten we dat er nog veel werk voor ons ligt. We zijn ons bewust van de soorten problemen (zowel prestaties als correctheid) die we proberen op te lossen, en zijn enthousiast over de nieuwe lay-outfuncties die naar CSS komen. Wij geloven dat de architectuur van LayoutNG het oplossen van deze problemen veilig en handelbaar maakt.

Eén afbeelding (je weet welke!) van Una Kravets .