RenderingNG deep-dive: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Blink verwijst naar Chromium's implementatie van het webplatform en omvat alle fasen van het renderen voorafgaand aan het compositing, culminerend in compositor commit . U kunt meer lezen over de blink-renderingarchitectuur in een vorig artikel in deze serie.

Blink begon zijn leven als een fork van WebKit , wat zelf een fork is van KHTML , die dateert uit 1998. Het bevat enkele van de oudste (en meest kritische) code in Chromium, en in 2014 was het duidelijk zijn ouderdom aan het tonen. In dat jaar zijn we begonnen aan een reeks ambitieuze projecten onder de vlag van wat we BlinkNG noemen, met als doel al lang bestaande tekortkomingen in de organisatie en structuur van de Blink-code aan te pakken. Dit artikel onderzoekt BlinkNG en de deelprojecten: waarom we ze hebben uitgevoerd, wat ze hebben bereikt, de leidende principes die hun ontwerp hebben gevormd en de mogelijkheden voor toekomstige verbeteringen die ze bieden.

De weergavepijplijn voor en na BlinkNG.

Rendering vóór NG

De weergavepijplijn binnen Blink was conceptueel altijd opgesplitst in fasen ( stijl , lay-out , verf , enzovoort), maar de abstractiebarrières waren lek. In grote lijnen bestonden de gegevens die verband hielden met weergave uit langlevende, veranderlijke objecten. Deze objecten konden op elk moment worden gewijzigd en werden regelmatig gerecycled en hergebruikt door opeenvolgende weergave-updates. Het was onmogelijk om eenvoudige vragen betrouwbaar te beantwoorden, zoals:

  • Moet de uitvoer van stijl, lay-out of verf worden bijgewerkt?
  • Wanneer krijgen deze gegevens hun ‘definitieve’ waarde?
  • Wanneer mag ik deze gegevens wijzigen?
  • Wanneer wordt dit object verwijderd?

Er zijn veel voorbeelden hiervan, waaronder:

Stijl genereert ComputedStyle bestanden op basis van stylesheets; maar ComputedStyle was niet onveranderlijk; in sommige gevallen zou het worden gewijzigd door latere pijplijnfasen.

Stijl zou een structuur van LayoutObject genereren, en vervolgens zou layout die objecten annoteren met informatie over grootte en positionering. In sommige gevallen zou de lay-out zelfs de boomstructuur wijzigen. Er was geen duidelijke scheiding tussen de in- en uitgangen van de lay-out .

Stijl zou aanvullende datastructuren genereren die het verloop van de compositie bepaalden, en die datastructuren werden in elke fase na stijl gewijzigd.

Op een lager niveau bestaan ​​de weergavegegevenstypen grotendeels uit gespecialiseerde bomen (bijvoorbeeld de DOM-boom, stijlboom, lay-outboom, verfeigenschappenboom); en weergavefasen worden geïmplementeerd als recursieve boomwandelingen. Idealiter zou er sprake moeten zijn van een boomwandeling: bij het verwerken van een bepaald boomknooppunt zouden we geen toegang moeten hebben tot enige informatie buiten de subboom die op dat knooppunt is geworteld. Dat was vóór RenderingNG nooit waar; boomwandelingen vaak gebruikte informatie van de voorouders van het knooppunt dat wordt verwerkt. Dit maakte het systeem erg kwetsbaar en foutgevoelig. Het was ook onmogelijk om ergens anders dan bij de wortel van de boom aan een boomwandeling te beginnen.

Ten slotte waren er veel verbeteringen in de renderingpijplijn verspreid over de code: geforceerde lay-outs geactiveerd door JavaScript, gedeeltelijke updates geactiveerd tijdens het laden van documenten, gedwongen updates ter voorbereiding op gebeurtenistargeting, geplande updates aangevraagd door het weergavesysteem en gespecialiseerde API's blootgelegd alleen om code te testen, om er maar een paar te noemen. Er waren zelfs een paar recursieve en herintredende paden in de weergavepijplijn (dat wil zeggen, naar het begin van de ene fase springen vanuit het midden van een andere). Elk van deze opritten had zijn eigen idiosyncratische gedrag, en in sommige gevallen zou de output van het renderen afhangen van de manier waarop de rendering-update werd geactiveerd.

Wat we veranderd hebben

BlinkNG bestaat uit vele deelprojecten, groot en klein, allemaal met het gedeelde doel om de eerder beschreven architectonische tekortkomingen weg te werken. Deze projecten delen een aantal leidende principes die zijn ontworpen om van de renderingpijplijn meer een echte pijplijn te maken:

  • Uniform toegangspunt : We moeten de pijplijn altijd aan het begin betreden.
  • Functionele fasen : Elke fase moet goed gedefinieerde inputs en outputs hebben, en het gedrag ervan moet functioneel zijn, dat wil zeggen deterministisch en herhaalbaar, en de outputs moeten alleen afhankelijk zijn van de gedefinieerde inputs.
  • Constante inputs : De inputs van elke trap moeten effectief constant zijn terwijl de trap loopt.
  • Onveranderlijke uitvoer : zodra een fase is voltooid, moeten de uitvoer ervan onveranderlijk zijn voor de rest van de weergave-update.
  • Controlepuntconsistentie : aan het einde van elke fase moeten de tot nu toe geproduceerde weergavegegevens in een zelfconsistente staat zijn.
  • Ontdubbeling van werk : bereken elk ding slechts één keer.

Een volledige lijst van BlinkNG-subprojecten zou vervelend lezen zijn, maar hier volgen er een paar die van bijzonder belang zijn.

De documentlevenscyclus

De klasse DocumentLifecycle houdt onze voortgang tijdens de weergavepijplijn bij. Het stelt ons in staat basiscontroles uit te voeren die de eerder genoemde invarianten afdwingen, zoals:

  • Als we een ComputedStyle-eigenschap wijzigen, moet de levenscyclus van het document kInStyleRecalc zijn.
  • Als de DocumentLifecycle-status kStyleClean of hoger is, moet NeedsStyleRecalc() false retourneren voor elk aangesloten knooppunt.
  • Bij het betreden van de verflevenscyclusfase moet de levenscyclusstatus kPrePaintClean zijn.

Tijdens de implementatie van BlinkNG hebben we systematisch codepaden geëlimineerd die deze invarianten schonden, en veel meer beweringen door de code verspreid om ervoor te zorgen dat we niet achteruitgaan.

Als je ooit in het konijnenhol hebt gezeten en naar renderingcode op laag niveau hebt gekeken, vraag je je misschien af: "Hoe ben ik hier terechtgekomen?" Zoals eerder vermeld, zijn er verschillende toegangspunten tot de renderingpijplijn. Voorheen omvatte dit recursieve en herintredende oproeppaden, en plaatsen waar we in een tussenfase in de pijplijn kwamen, in plaats van vanaf het begin te beginnen. In de loop van BlinkNG hebben we deze oproeppaden geanalyseerd en vastgesteld dat ze allemaal te herleiden zijn tot twee basisscenario’s:

  • Alle weergavegegevens moeten worden bijgewerkt, bijvoorbeeld bij het genereren van nieuwe pixels voor weergave of het uitvoeren van een hittest voor gebeurtenistargeting.
  • We hebben een actuele waarde nodig voor een specifieke vraag die kan worden beantwoord zonder alle weergavegegevens bij te werken. Dit omvat de meeste JavaScript-query's, bijvoorbeeld node.offsetTop .

Er zijn nu slechts twee toegangspunten tot de renderingpijplijn, die overeenkomen met deze twee scenario's. De herintredende codepaden zijn verwijderd of geherstructureerd en het is niet langer mogelijk om vanaf een tussenfase in de pijplijn te komen. Dit heeft veel mysterie geëlimineerd rond wanneer en hoe weergave-updates precies plaatsvinden, waardoor het veel gemakkelijker wordt om over het gedrag van het systeem te redeneren.

Pipeliningstijl, lay-out en pre-painting

Gezamenlijk zijn de renderingfasen vóór het schilderen verantwoordelijk voor het volgende:

  • Het stijlcascade- algoritme uitvoeren om de uiteindelijke stijleigenschappen voor DOM-knooppunten te berekenen.
  • Het genereren van de lay-outboom die de kaderhiërarchie van het document vertegenwoordigt.
  • Bepalen van maat- en positie-informatie voor alle dozen.
  • Afronding of snapping van subpixelgeometrie naar hele pixelgrenzen voor schilderen.
  • Het bepalen van de eigenschappen van samengestelde lagen (affiene transformatie, filters, dekking of iets anders dat GPU-versneld kan worden).
  • Bepalen welke inhoud er is veranderd sinds de vorige verffase en moet worden geverfd of opnieuw moet worden geverfd (verfinvalidatie).

Deze lijst is niet veranderd, maar vóór BlinkNG werd veel van dit werk op een ad hoc manier gedaan, verspreid over meerdere weergavefasen, met veel dubbele functionaliteit en ingebouwde inefficiënties. De stijlfase is bijvoorbeeld altijd primair verantwoordelijk geweest voor het berekenen van de uiteindelijke stijleigenschappen voor knooppunten, maar er waren een paar speciale gevallen waarin we de definitieve stijleigenschapswaarden pas bepaalden nadat de stijlfase was voltooid. Er was geen formeel of afdwingbaar punt in het weergaveproces waarop we met zekerheid konden zeggen dat stijlinformatie compleet en onveranderlijk was.

Een ander goed voorbeeld van pre-BlinkNG-problemen is het ongeldig maken van verf. Voorheen was verfinvalidatie verspreid over alle renderingfasen voorafgaand aan de verf. Bij het wijzigen van stijl- of lay-outcode was het moeilijk om te weten welke wijzigingen in de ongeldigverklaringslogica van de verf nodig waren, en het was gemakkelijk om een ​​fout te maken die leidde tot fouten bij het te weinig of te veel valideren. U kunt meer lezen over de fijne kneepjes van het oude verfinvalidatiesysteem in het artikel uit deze serie gewijd aan LayoutNG .

Het snappen van de geometrie van de subpixellay-out naar hele pixelgrenzen voor schilderen is een voorbeeld van een situatie waarin we meerdere implementaties van dezelfde functionaliteit hadden en veel overtollig werk deden. Er werd één pixel-snapping-codepad gebruikt door het verfsysteem, en een volledig afzonderlijk codepad dat werd gebruikt wanneer we een eenmalige, on-the-fly berekening nodig hadden van pixel-snapped-coördinaten buiten de verfcode. Het is onnodig om te zeggen dat elke implementatie zijn eigen bugs had, en de resultaten kwamen niet altijd overeen. Omdat deze informatie niet in de cache werd opgeslagen, voerde het systeem soms herhaaldelijk exact dezelfde berekening uit, wat de prestaties opnieuw onder druk zette.

Hier zijn enkele belangrijke projecten die de architectonische tekortkomingen van de renderingfasen voorafgaand aan het schilderen hebben geëlimineerd.

Project Squad: Pipelining van de stijlfase

Dit project pakte twee belangrijke tekortkomingen in de stijlfase aan, waardoor het project niet netjes van de pijplijn kon komen:

Er zijn twee primaire uitvoer van de stijlfase: ComputedStyle , met het resultaat van het uitvoeren van het CSS-cascade-algoritme over de DOM-boom; en een boom van LayoutObjects , die de volgorde van de bewerkingen voor de lay-outfase vaststelt. Conceptueel gezien zou het uitvoeren van het cascade-algoritme strikt moeten gebeuren voordat de lay-outboom wordt gegenereerd; maar voorheen werden deze twee operaties afgewisseld. Project Squad slaagde erin deze twee op te splitsen in afzonderlijke, opeenvolgende fasen.

Voorheen kreeg ComputedStyle niet altijd de uiteindelijke waarde tijdens het opnieuw berekenen van de stijl; er waren een paar situaties waarin ComputedStyle werd bijgewerkt tijdens een latere pijplijnfase. Project Squad heeft deze codepaden met succes opnieuw vormgegeven, zodat ComputedStyle na de stijlfase nooit meer wordt gewijzigd.

LayoutNG: Pipelining van de lay-outfase

Dit monumentale project – een van de hoekstenen van RenderingNG – was een volledige herschrijving van de fase van het renderen van de lay-out. We zullen hier geen recht doen aan het hele project, maar er zijn een paar opmerkelijke aspecten voor het totale BlinkNG-project:

  • Voorheen ontving de lay-outfase een structuur van LayoutObject gemaakt door de stijlfase, en werd de boom geannoteerd met informatie over grootte en positie. Er was dus geen sprake van een zuivere scheiding tussen input en output. LayoutNG introduceerde de fragmentboom , die de primaire, alleen-lezen uitvoer van layout is, en dient als de primaire invoer voor daaropvolgende weergavefasen.
  • LayoutNG bracht de containment-eigenschap naar layout: bij het berekenen van de grootte en positie van een gegeven LayoutObject kijken we niet langer buiten de subboom die op dat object is geworteld. Alle informatie die nodig is om de lay-out voor een bepaald object bij te werken, wordt vooraf berekend en als alleen-lezen invoer aan het algoritme geleverd.
  • Voorheen waren er randgevallen waarin het lay-outalgoritme niet strikt functioneel was: het resultaat van het algoritme was afhankelijk van de meest recente eerdere lay-outupdate. LayoutNG heeft deze gevallen geëlimineerd.

De voorverffase

Voorheen was er geen formele renderingfase voorafgaand aan het schilderen, maar alleen een greep uit de post-lay-outwerkzaamheden. De fase vóór het schilderen kwam voort uit de erkenning dat er een paar gerelateerde functies waren die het beste konden worden geïmplementeerd als een systematische doorloop van de lay-outboom nadat de lay-out voltooid was; het allerbelangrijkste:

  • Het uitvoeren van verfinvalidaties : Het is erg moeilijk om verfinvalidatie correct uit te voeren tijdens de lay-out, als we over onvolledige informatie beschikken. Het is veel gemakkelijker om het goed te doen, en kan zeer efficiënt zijn, als het in twee verschillende processen wordt opgesplitst: tijdens stijl en lay-out kan de inhoud worden gemarkeerd met een eenvoudige Booleaanse vlag als "mogelijk moet de verf ongeldig worden gemaakt." Tijdens de pre-paint-bomenwandeling controleren we deze vlaggen en geven we indien nodig ongeldigverklaringen af.
  • Het genereren van verfeigenschapsbomen : een proces dat verderop in meer detail wordt beschreven.
  • Berekenen en vastleggen van pixel-snapped verflocaties : De geregistreerde resultaten kunnen worden gebruikt door de verffase, en ook door elke stroomafwaartse code die ze nodig heeft, zonder enige redundante berekening.

Eigenschappenbomen: consistente geometrie

Eigendomsbomen werden al vroeg in RenderingNG geïntroduceerd om om te gaan met de complexiteit van scrollen, dat op internet een andere structuur heeft dan alle andere soorten visuele effecten. Vóór de eigenschapsbomen gebruikte de compositor van Chromium een ​​enkele 'laag'-hiërarchie om de geometrische relatie van samengestelde inhoud weer te geven, maar die viel snel uiteen toen de volledige complexiteit van functies zoals positie:vast duidelijk werd. De lagenhiërarchie kreeg extra niet-lokale verwijzingen die de "scroll parent" of "clip parent" van een laag aanduiden, en het duurde niet lang voordat het erg moeilijk werd om de code te begrijpen.

Property Trees hebben dit opgelost door de overflow-scroll- en clip-aspecten van de inhoud afzonderlijk van alle andere visuele effecten weer te geven. Dit maakte het mogelijk om de echte visuele en scrollende structuur van websites correct te modelleren. Vervolgens hoefden we "alles" te doen om algoritmen bovenop de eigenschappenbomen te implementeren, zoals de schermruimte-transformatie van samengestelde lagen, of het bepalen welke lagen scrolden en welke niet.

We merkten al snel dat er veel andere plaatsen in de code waren waar vergelijkbare geometrische vragen werden gesteld. (Het bericht over de belangrijkste datastructuren bevat een completere lijst.) Verschillende daarvan hadden dubbele implementaties van hetzelfde wat de compositorcode deed; ze hadden allemaal een andere subset van bugs; en geen van hen heeft de echte websitestructuur goed gemodelleerd. De oplossing werd toen duidelijk: centraliseer alle geometrie-algoritmen op één plek en refactoreer alle code om deze te gebruiken.

Deze algoritmen zijn op hun beurt allemaal afhankelijk van eigendomsbomen. Daarom zijn eigendomsbomen een belangrijke datastructuur (dat wil zeggen, een structuur die in de pijplijn van RenderingNG wordt gebruikt). Om dit doel van gecentraliseerde geometriecode te bereiken, moesten we het concept van eigenschapsbomen veel eerder in de pijplijn introduceren (in pre-paint) en alle API's die er nu van afhankelijk waren, zodanig wijzigen dat pre-paint moest worden uitgevoerd voordat ze konden worden uitgevoerd. .

Dit verhaal is nog een ander aspect van het BlinkNG-refactoringpatroon: identificeer belangrijke berekeningen, refactoreer om te voorkomen dat ze worden gedupliceerd, en creëer goed gedefinieerde pijplijnfasen die de datastructuren creëren die ze voeden. We berekenen eigendomsbomen precies op het punt waarop alle benodigde informatie beschikbaar is; en we zorgen ervoor dat de eigenschappenbomen niet kunnen veranderen terwijl latere weergavefasen worden uitgevoerd.

Composiet na verf: verf in pijpleidingen aanbrengen en composteren

Gelaagdheid is het proces waarbij wordt uitgezocht welke DOM-inhoud in zijn eigen samengestelde laag terechtkomt (die op zijn beurt een GPU-textuur vertegenwoordigt). Vóór RenderingNG liep de gelaagdheid vóór het schilderen, niet erna (zie hier voor de huidige pijplijn – let op de wijziging van de volgorde). We zouden eerst beslissen welke delen van de DOM in welke samengestelde laag terecht zouden komen, en pas daarna weergavelijsten voor die texturen tekenen. Uiteraard waren de beslissingen afhankelijk van factoren zoals welke DOM-elementen animeerden of scrollden, of 3D-transformaties hadden, en welke elementen daarop werden geschilderd.

Dit veroorzaakte grote problemen, omdat het min of meer vereiste dat er circulaire afhankelijkheden in de code zaten, wat een groot probleem is voor een renderingpijplijn. Laten we aan de hand van een voorbeeld zien waarom. Stel dat we verf ongeldig moeten maken (wat betekent dat we de weergavelijst opnieuw moeten tekenen en deze vervolgens opnieuw moeten rasteren). De noodzaak om te valideren kan voortkomen uit een verandering in de DOM, of uit een gewijzigde stijl of lay-out. Maar natuurlijk willen we alleen de delen ongeldig maken die daadwerkelijk zijn veranderd. Dat betekende dat we moesten uitzoeken welke samengestelde lagen getroffen waren, en vervolgens een deel of alle weergavelijsten voor die lagen ongeldig moesten maken.

Dit betekent dat de invalidatie afhankelijk was van DOM, stijl, lay-out en eerdere beslissingen over laagvorming (verleden: betekenis voor het vorige gerenderde frame). Maar de huidige gelaagdheid hangt ook van al deze dingen af. En omdat we niet over twee kopieën van alle gelaagdheidsgegevens beschikten, was het moeilijk om het verschil te zien tussen eerdere en toekomstige gelaagdheidsbeslissingen. We eindigden dus met veel code met een cirkelredenering. Dit leidde soms tot onlogische of onjuiste code, of zelfs crashes of beveiligingsproblemen, als we niet erg voorzichtig waren.

Om met deze situatie om te gaan, hebben we al vroeg het concept van het DisableCompositingQueryAsserts -object geïntroduceerd. Als code probeerde eerdere gelaagdheidsbeslissingen op te vragen, veroorzaakte dit meestal een beweringsfout en crashte de browser als deze zich in de foutopsporingsmodus bevond. Dit heeft ons geholpen om de introductie van nieuwe bugs te voorkomen. En in elk geval waarin de code op legitieme wijze vragen moest stellen over eerdere gelaagdheidsbeslissingen, hebben we code ingevoegd om dit mogelijk te maken door een DisableCompositingQueryAsserts object toe te wijzen.

Ons plan was om na verloop van tijd alle DisableCompositingQueryAssert objecten van call-sites te verwijderen en de code vervolgens veilig en correct te verklaren. Maar wat we ontdekten is dat een aantal van de afzettingen in wezen onmogelijk te verwijderen waren, zolang er vóór het schilderen sprake was van gelaagdheid. (We konden het uiteindelijk pas zeer onlangs verwijderen!) Dit was de eerste reden die werd ontdekt voor het Composite After Paint-project. Wat we leerden was dat, zelfs als je een goed gedefinieerde pijplijnfase voor een operatie hebt, je uiteindelijk vastloopt als deze zich op de verkeerde plaats in de pijplijn bevindt.

De tweede reden voor het Composite After Paint-project was de Fundamental Compositing-bug. Eén manier om deze bug te benoemen is dat DOM-elementen geen goede 1:1-weergave zijn van een efficiënt of compleet gelaagdheidsschema voor de inhoud van webpagina's. En aangezien compositie vóór verf plaatsvond, was het min of meer inherent afhankelijk van DOM-elementen, en niet van weergavelijsten of eigendomsbomen. Dit komt sterk overeen met de reden waarom we eigendomsbomen hebben geïntroduceerd, en net als bij eigendomsbomen valt de oplossing direct uit als je de juiste pijplijnfase uitzoekt, deze op het juiste moment uitvoert en deze voorziet van de juiste belangrijke datastructuren. En net als bij eigendomsbomen was dit een goede gelegenheid om te garanderen dat zodra de verffase is voltooid, de output ervan onveranderlijk is voor alle volgende pijplijnfasen.

Voordelen

Zoals u heeft gezien, levert een goed gedefinieerde renderingpijplijn enorme voordelen op de lange termijn op. Er zijn er zelfs meer dan je zou denken:

  • Sterk verbeterde betrouwbaarheid : deze is vrij eenvoudig. Schonere code met goed gedefinieerde en begrijpelijke interfaces is gemakkelijker te begrijpen, schrijven en testen. Dit maakt het betrouwbaarder. Het maakt de code ook veiliger en stabieler, met minder crashes en minder use-after-free bugs.
  • Uitgebreide testdekking : in de loop van BlinkNG hebben we een groot aantal nieuwe tests aan onze suite toegevoegd. Dit omvat unit-tests die gerichte verificatie van interne onderdelen bieden; regressietests die voorkomen dat we oude bugs opnieuw introduceren die we hebben opgelost (zoveel!); en veel toevoegingen aan de openbare, gezamenlijk onderhouden Web Platform Test-suite , die alle browsers gebruiken om de conformiteit met webstandaarden te meten.
  • Gemakkelijker uit te breiden : Als een systeem is opgesplitst in duidelijke componenten, is het niet nodig om andere componenten op welk detailniveau dan ook te begrijpen om vooruitgang te boeken met de huidige. Dit maakt het voor iedereen gemakkelijker om waarde toe te voegen aan de weergavecode zonder een diepgaande expert te hoeven zijn, en het maakt het ook gemakkelijker om te redeneren over het gedrag van het hele systeem.
  • Prestaties : Het optimaliseren van algoritmen geschreven in spaghetticode is al moeilijk genoeg, maar het is bijna onmogelijk om nog grotere dingen te bereiken, zoals universeel scrollen en animaties met threads of de processen en threads voor site-isolatie zonder een dergelijke pijplijn. Parallellisme kan ons helpen de prestaties enorm te verbeteren, maar is ook uiterst ingewikkeld.
  • Opbrengst en beheersing : Er zijn verschillende nieuwe functies mogelijk gemaakt door BlinkNG die de pijplijn op nieuwe en nieuwe manieren uitoefenen. Wat als we bijvoorbeeld de renderingpijplijn alleen willen laten draaien totdat een budget is verlopen? Of kunt u de weergave overslaan voor substructuren waarvan bekend is dat ze op dit moment niet relevant zijn voor de gebruiker? Dat is wat de CSS-eigenschap voor inhoudzichtbaarheid mogelijk maakt. Hoe zit het met het afhankelijk maken van de stijl van een component van de lay-out? Dat zijn containerquery's .

Casestudy: containerquery's

Containerquery's zijn een langverwachte nieuwe webplatformfunctie (het is al jaren de meest gevraagde functie van CSS-ontwikkelaars). Als het zo geweldig is, waarom bestaat het dan nog niet? De reden is dat een implementatie van containerquery's een zeer zorgvuldig begrip en controle van de relatie tussen de stijl en de lay-outcode vereist. Laten we dat eens van dichterbij bekijken.

Met een containerquery kunnen de stijlen die op een element van toepassing zijn, afhankelijk zijn van de lay-outgrootte van een voorouder. Omdat de lay-outgrootte tijdens de lay-out wordt berekend, betekent dit dat we na de lay-out stijlherberekening moeten uitvoeren; maar stijlherberekening wordt vóór de lay-out uitgevoerd ! Deze kip-en-ei-paradox is de hele reden waarom we vóór BlinkNG geen containerquery's konden implementeren.

Hoe kunnen we dit oplossen? Is het niet een achterwaartse pijplijnafhankelijkheid, dat wil zeggen hetzelfde probleem dat projecten als Composite After Paint hebben opgelost? Erger nog, wat als de nieuwe stijlen de grootte van de voorouder veranderen? Leidt dit soms niet tot een oneindige lus?

In principe kan de circulaire afhankelijkheid worden opgelost door gebruik te maken van de eigenschap contain CSS, waardoor weergave buiten een element niet afhankelijk is van weergave binnen de subboom van dat element . Dat betekent dat de nieuwe stijlen die door een container worden toegepast geen invloed kunnen hebben op de grootte van de container, omdat containerquery's containment vereisen .

Maar eigenlijk was dat niet genoeg, en was het noodzakelijk om een ​​zwakker soort inperking te introduceren dan alleen de omvang van de insluiting. Dit komt omdat het gebruikelijk is dat een containerquerycontainer het formaat in slechts één richting (meestal blok) kan wijzigen op basis van de inline-afmetingen. Daarom werd het concept van inline-size containment toegevoegd. Maar zoals je kunt zien in de zeer lange notitie in dat gedeelte, was het lange tijd helemaal niet duidelijk of inline-groottebeheersing mogelijk was.

Het is één ding om containment in abstracte spec-taal te beschrijven, maar het is iets heel anders om het correct te implementeren. Bedenk dat een van de doelstellingen van BlinkNG was om het containmentprincipe te introduceren in de boomwandelingen die de belangrijkste weergavelogica vormen: bij het doorlopen van een subboom mag er geen informatie van buiten de subboom vereist zijn. Toevallig (nou ja, het was niet bepaald een ongeluk), is het veel schoner en gemakkelijker om CSS-insluiting te implementeren als de weergavecode zich aan het insluitingsprincipe houdt.

Toekomst: compositing buiten de hoofdthread … en verder!

De hier getoonde renderingpijplijn loopt eigenlijk een beetje voor op de huidige RenderingNG-implementatie. Het laat zien dat de gelaagdheid buiten de hoofdthread ligt, terwijl deze momenteel nog steeds op de hoofdthread zit. Het is echter slechts een kwestie van tijd voordat dit klaar is, nu Composite After Paint is verzonden en de gelaagdheid na verf komt.

Om te begrijpen waarom dit belangrijk is, en waar het nog meer toe kan leiden, moeten we de architectuur van de rendering-engine vanuit een iets hoger gezichtspunt bekijken. Een van de meest duurzame obstakels voor het verbeteren van de prestaties van Chromium is het simpele feit dat de hoofdthread van de renderer zowel de hoofdtoepassingslogica (dat wil zeggen het uitvoeren van scripts) als het grootste deel van de weergave afhandelt. Als gevolg hiervan is de hoofdthread vaak verzadigd met werk, en is congestie van de hoofdthread vaak het knelpunt in de hele browser.

Het goede nieuws is dat dit niet zo hoeft te zijn! Dit aspect van de architectuur van Chromium dateert helemaal uit de tijd van KHTML , toen single-threaded uitvoering het dominante programmeermodel was. Tegen de tijd dat multi-coreprocessors gemeengoed werden in apparaten van consumentenkwaliteit, was de single-threaded-aanname grondig ingebakken in Blink (voorheen WebKit). We wilden al heel lang meer threading in de rendering-engine introduceren, maar dat was simpelweg onmogelijk in het oude systeem. Een van de belangrijkste doelstellingen van Rendering NG was om onszelf uit dit gat te halen en het mogelijk te maken om renderingwerk, geheel of gedeeltelijk, naar een andere thread (of threads) te verplaatsen.

Nu BlinkNG zijn voltooiing nadert, beginnen we dit gebied al te verkennen; Non-Blocking Commit is een eerste stap in het veranderen van het threadingmodel van de renderer. Compositor commit (of gewoon commit ) is een synchronisatiestap tussen de hoofdthread en de compositorthread. Tijdens het vastleggen maken we kopieën van weergavegegevens die op de hoofdthread worden geproduceerd, om te worden gebruikt door de downstream compositingcode die op de compositorthread wordt uitgevoerd. Terwijl deze synchronisatie plaatsvindt, wordt de uitvoering van de hoofdthread gestopt terwijl de kopieercode op de compositorthread wordt uitgevoerd. Dit wordt gedaan om ervoor te zorgen dat de hoofdthread de weergavegegevens niet wijzigt terwijl de compositor-thread deze kopieert.

Niet-blokkerende commit elimineert de noodzaak dat de hoofdthread stopt en wacht tot de commit-fase is afgelopen; de hoofdthread blijft werken terwijl commit gelijktijdig op de compositor-thread wordt uitgevoerd. Het netto-effect van Non-Blocking Commit zal een vermindering zijn van de tijd die wordt besteed aan het renderen van werk aan de hoofdthread, waardoor de congestie op de hoofdthread zal afnemen en de prestaties zullen verbeteren. Op het moment dat we dit schrijven (maart 2022) hebben we een werkend prototype van Non-Blocking Commit en bereiden we ons voor op een gedetailleerde analyse van de impact ervan op de prestaties.

Wachten in de coulissen is Off-main-thread Compositing , met als doel de rendering-engine te laten matchen met de illustratie door de gelaagdheid van de hoofdthread naar een werkthread te verplaatsen. Net als Non-Blocking Commit zal dit de congestie op de hoofdthread verminderen door de weergavewerklast te verminderen. Een project als dit zou nooit mogelijk zijn geweest zonder de architectonische verbeteringen van Composite After Paint.

En er zitten nog meer projecten in de pijplijn (woordspeling bedoeld)! We hebben eindelijk een stichting die het mogelijk maakt om te experimenteren met het herverdelen van renderingwerk, en we zijn erg enthousiast om te zien wat er allemaal mogelijk is!