Overzicht van de RenderingNG-architectuur

Chris Harrelson
Chris Harrelson

In een vorige post gaf ik een overzicht van de RenderingNG-architectuurdoelen en belangrijkste eigenschappen. In dit bericht wordt uitgelegd hoe de samenstellende delen zijn opgezet en hoe de weergavepijplijn er doorheen stroomt.

Beginnend op het hoogste niveau en van daaruit verdergaand, zijn de taken van het renderen:

  1. Render inhoud in pixels op het scherm.
  2. Animeer visuele effecten op de inhoud van de ene staat naar de andere.
  3. Scroll als reactie op invoer.
  4. Leid invoer efficiënt naar de juiste plaatsen, zodat ontwikkelaarsscripts en andere subsystemen kunnen reageren.

De inhoud die moet worden weergegeven, bestaat uit een boomstructuur met frames voor elk browsertabblad, plus de browsergebruikersinterface. En een stroom ruwe invoergebeurtenissen van aanraakschermen, muizen, toetsenborden en andere hardwareapparaten.

Elk frame bevat:

  • DOM-staat
  • CSS
  • Doeken
  • Externe bronnen, zoals afbeeldingen, video, lettertypen en SVG

Een frame is een HTML-document, plus de URL ervan. Een webpagina die op een browsertabblad is geladen, heeft een frame op het hoogste niveau, onderliggende frames voor elk iframe dat is opgenomen in het document op het hoogste niveau, en hun recursieve iframe-afstammelingen.

Een visueel effect is een grafische bewerking die op een bitmap wordt toegepast, zoals scrollen, transformeren, knippen, filteren, dekking of overvloeien.

Architectuurcomponenten

In RenderingNG zijn deze taken logisch verdeeld over verschillende fasen en codecomponenten. De componenten komen terecht in verschillende CPU-processen, threads en subcomponenten binnen die threads. Ze spelen allemaal een belangrijke rol bij het bereiken van betrouwbaarheid , schaalbare prestaties en uitbreidbaarheid voor alle webinhoud.

Pijpleidingstructuur renderen

Diagram van de weergavepijplijn zoals uitgelegd in de volgende tekst.

Het renderen verloopt in een pijplijn, waarbij onderweg een aantal fasen en artefacten worden gecreëerd. Elke fase vertegenwoordigt code die één goed gedefinieerde taak binnen het renderen uitvoert. De artefacten zijn datastructuren die input of output zijn van de fasen; in het diagram worden ingangen of uitgangen aangegeven met pijlen.

Deze blogpost gaat niet veel in detail over de artefacten; dat zal in het volgende bericht worden besproken: Key Data Structures en hun rol in RenderingNG .

De pijplijnfasen

In het voorgaande diagram worden fasen aangegeven met kleuren die aangeven in welke thread of proces ze worden uitgevoerd:

  • Groen: rode draad in renderproces
  • Geel: renderprocescompositor
  • Oranje: namelijk proces

In sommige gevallen kunnen ze op meerdere plaatsen worden uitgevoerd, afhankelijk van de omstandigheid. Daarom hebben sommige twee kleuren.

De fasen zijn:

  1. Animeren: verander berekende stijlen en muteer eigendomsbomen in de loop van de tijd op basis van declaratieve tijdlijnen.
  2. Stijl: pas CSS toe op de DOM en maak berekende stijlen .
  3. Lay-out: bepaal de grootte en positie van DOM-elementen op het scherm en creëer de onveranderlijke fragmentboom .
  4. Voorschilderen: bereken eigenschappenbomen en maak eventuele bestaande weergavelijsten en GPU- textuurtegels ongeldig .
  5. Scrollen: update de scroll-offset van documenten en scrollbare DOM-elementen door eigenschappenbomen te muteren.
  6. Verf: bereken een weergavelijst die beschrijft hoe GPU-textuurtegels vanuit de DOM moeten worden gerasterd.
  7. Commit: kopieer eigenschappenbomen en de weergavelijst naar de compositor-thread.
  8. Gelaagd maken: deel de weergavelijst op in een samengestelde lagenlijst voor onafhankelijke rastering en animatie.
  9. Raster-, decodeer- en verfwerkjes: zet respectievelijk weergavelijsten, gecodeerde afbeeldingen en verfwerkletcode om in GPU-textuurtegels .
  10. Activeren: maak een compositorframe dat weergeeft hoe GPU-tegels op het scherm moeten worden getekend en gepositioneerd, samen met eventuele visuele effecten.
  11. Aggregeren: combineer compositorframes van alle zichtbare compositorframes in één enkel, globaal compositorframe.
  12. Tekenen: voer het geaggregeerde compositorframe uit op de GPU om pixels op het scherm te creëren.

Fasen van de weergavepijplijn kunnen worden overgeslagen als ze niet nodig zijn. Animaties van visuele effecten en scrollen kunnen bijvoorbeeld de lay-out, pre-paint en paint overslaan. Daarom zijn animatie en scrollen in het diagram gemarkeerd met gele en groene stippen. Als layout, pre-paint en paint kunnen worden overgeslagen voor visuele effecten, kunnen ze volledig op de compositor-thread worden uitgevoerd en de hoofdthread worden overgeslagen.

De weergave van de browser-UI wordt hier niet direct weergegeven, maar kan worden gezien als een vereenvoudigde versie van dezelfde pijplijn (en in feite deelt de implementatie ervan een groot deel van de code). Video (ook niet direct afgebeeld) wordt doorgaans weergegeven via onafhankelijke code die frames decodeert in GPU-textuurtegels die vervolgens worden aangesloten op compositorframes en de tekenstap.

Proces- en draadstructuur

CPU-processen

Het gebruik van meerdere CPU-processen zorgt voor prestatie- en beveiligingsisolatie tussen sites en van de browserstatus, en stabiliteit en beveiligingsisolatie van GPU-hardware.

Diagram van de verschillende delen van de CPU-processen

  • Het weergaveproces rendert, animeert, scrollt en routeert invoer voor een enkele combinatie van site en tabblad. Er zijn veel renderprocessen.
  • Het browserproces rendert, animeert en stuurt invoer voor de gebruikersinterface van de browser (inclusief de URL-balk, tabbladtitels en pictogrammen) en stuurt alle resterende invoer naar het juiste weergaveproces. Er is precies één browserproces.
  • Het Viz-proces verzamelt composities van meerdere weergaveprocessen plus het browserproces. Het rastert en tekent met behulp van de GPU. Er is precies één Viz-proces.

Verschillende sites eindigen altijd in verschillende weergaveprocessen. (In werkelijkheid altijd op desktop; indien mogelijk op mobiel . Ik zal hieronder 'altijd' schrijven, maar dit voorbehoud is overal van toepassing.)

Meerdere browsertabbladen of vensters van dezelfde site gebruiken meestal verschillende weergaveprocessen, tenzij de tabbladen gerelateerd zijn (de ene opent de andere). Onder sterke geheugendruk op de desktop kan Chromium meerdere tabbladen van dezelfde site in hetzelfde weergaveproces plaatsen, zelfs als ze niet gerelateerd zijn.

Binnen één browsertabblad bevinden frames van verschillende sites zich altijd in verschillende weergaveprocessen, maar frames van dezelfde site bevinden zich altijd in hetzelfde weergaveproces. Vanuit het perspectief van weergave is het belangrijke voordeel van meerdere weergaveprocessen dat cross-site iframes en tabbladen prestatie-isolatie van elkaar bereiken. Bovendien kunnen origines kiezen voor nog meer isolatie .

Er is precies één Viz-proces voor heel Chromium. Er is tenslotte meestal maar één GPU en scherm om op te tekenen. Het scheiden van Viz in zijn eigen proces is goed voor de stabiliteit in het geval van bugs in GPU-stuurprogramma's of hardware. Het is ook goed voor beveiligingsisolatie, wat belangrijk is voor GPU-API's zoals Vulkan . Het is ook belangrijk voor de veiligheid in het algemeen .

Omdat de browser veel tabbladen en vensters kan hebben, en ze allemaal browser-UI-pixels hebben om te tekenen, vraag je je misschien af: waarom is er precies één browserproces? De reden is dat slechts één van hen tegelijk gefocust is; in feite zijn niet-zichtbare browsertabbladen meestal gedeactiveerd en wordt al hun GPU-geheugen verwijderd. Complexe browser-UI-renderingfuncties worden echter ook steeds vaker geïmplementeerd in renderprocessen (bekend als WebUI ). Dit is niet om redenen van prestatie-isolatie, maar om te profiteren van het gebruiksgemak van de webweergave-engine van Chromium.

Op oudere Android-apparaten worden het weergave- en browserproces gedeeld bij gebruik in een WebView (dit geldt niet voor Chromium op Android in het algemeen, alleen voor WebView). Op WebView wordt het browserproces ook gedeeld met de insluitingsapp, en WebView heeft slechts één weergaveproces.

Er is soms ook een hulpprogramma voor het decoderen van beveiligde video-inhoud. Dit proces is hierboven niet weergegeven.

Draden

Threads helpen prestatie-isolatie en reactievermogen te bereiken, ondanks langzame taken, parallellisatie van pijplijnen en meervoudige buffering.

Een diagram van het renderproces zoals beschreven in het artikel.

  • De rode draad voert scripts uit, de rendering-gebeurtenislus, de documentlevenscyclus, hittests, verzending van scriptgebeurtenissen en het parseren van HTML, CSS en andere gegevensformaten.
    • Hoofdthreadhelpers voeren taken uit zoals het maken van afbeeldingsbitmaps en blobs waarvoor codering of decodering vereist is.
    • Web Workers voeren een script uit en een rendering-gebeurtenislus voor OffscreenCanvas.
  • De Compositor-thread verwerkt invoergebeurtenissen, voert scrollen en animaties van webinhoud uit, berekent de optimale gelaagdheid van webinhoud en coördineert beelddecodering, verfwerkjes en rastertaken.
    • Compositor-threadhelpers coördineren Viz-rastertaken en voeren afbeeldingsdecoderingstaken uit, verfworklets en fallback-raster.
  • Media-, demuxer- of audio-uitvoerthreads decoderen, verwerken en synchroniseren video- en audiostreams. (Houd er rekening mee dat video parallel wordt uitgevoerd met de hoofdrenderingpijplijn.)

Het scheiden van de hoofdthreads en de compositorthreads is van cruciaal belang voor de prestatie-isolatie van animatie en scrollen van hoofdthreadwerk.

Er is slechts één rode draad per weergaveproces, ook al kunnen meerdere tabbladen of frames van dezelfde site in hetzelfde proces terechtkomen. Er is echter prestatie-isolatie van werk dat wordt uitgevoerd in verschillende browser-API's. Het genereren van afbeeldingsbitmaps en blobs in de Canvas API wordt bijvoorbeeld uitgevoerd in een hoofdthread-helperthread.

Op dezelfde manier is er slechts één compositor-thread per weergaveproces. Het is over het algemeen geen probleem dat er maar één is, omdat alle erg dure bewerkingen op de compositor-thread worden gedelegeerd naar de compositor-werkthreads of het Viz-proces, en dit werk kan parallel worden gedaan met invoerroutering, scrollen of animatie. . Compositor-werkthreads coördineren de taken die in het Viz-proces worden uitgevoerd, maar GPU-versnelling kan overal mislukken om redenen buiten de controle van Chromium, zoals bugs in stuurprogramma's. In deze situaties zal de werkthread het werk doen in een fallback-modus op de CPU.

Het aantal compositor-werkthreads is afhankelijk van de mogelijkheden van het apparaat. Desktops zullen bijvoorbeeld over het algemeen meer threads gebruiken, omdat ze meer CPU-kernen hebben en minder batterijbelast zijn dan mobiele apparaten. Dit is een voorbeeld van opschalen en afschalen .

Het is ook interessant om op te merken dat de threading-architectuur van het renderproces een toepassing is van drie verschillende optimalisatiepatronen:

  • Hulpthreads: het verzenden van langlopende subtaken naar extra threads, om de bovenliggende thread te laten reageren op andere verzoeken die tegelijkertijd plaatsvinden. De hoofdthreadhelper- en compositorhelperthreads zijn goede voorbeelden van deze techniek.
  • Meervoudige buffering : eerder weergegeven inhoud weergeven terwijl nieuwe inhoud wordt weergegeven, om de latentie van weergave te verbergen. De compositor-thread gebruikt deze techniek.
  • Parallellisatie van pijpleidingen: het uitvoeren van de weergavepijplijn op meerdere plaatsen tegelijk. Dit is de manier waarop scrollen en animatie snel kunnen zijn, zelfs als er een update voor de weergave van de hoofdthread plaatsvindt, omdat scrollen en animatie parallel kunnen worden uitgevoerd.

Browserproces

Een browserprocesdiagram dat de relatie toont tussen de Render- en compositing-thread, en de render- en compositing-thread-helper.

  • De render- en compositingthread reageert op invoer in de gebruikersinterface van de browser en leidt andere invoer naar het juiste weergaveproces; lay-out en schildert de browser-UI.
  • De render- en compositing-thread-helpers voeren afbeeldingsdecoderingstaken uit en fallback-raster of decodering.

De render- en compositingthread van het browserproces zijn vergelijkbaar met de code en functionaliteit van een renderproces, behalve dat de hoofdthread en de compositorthread in één zijn gecombineerd. Er is in dit geval slechts één thread nodig omdat er geen behoefte is aan prestatie-isolatie van lange hoofdthreadtaken, aangezien deze er niet zijn door het ontwerp.

Zie proces

Een diagram dat laat zien dat het Viz-proces de GPU-hoofdthread en de displaycompositor-thread omvat.

  • De GPU-hoofdthreadrasters geven lijsten en videoframes weer in GPU-textuurtegels, en tekenen compositorframes naar het scherm.
  • De display-compositor-thread verzamelt en optimaliseert de compositie van elk weergaveproces, plus het browserproces, in een enkel compositor-frame voor presentatie op het scherm.

Raster en draw gebeuren over het algemeen op dezelfde thread, omdat ze allebei afhankelijk zijn van GPU-bronnen, en het moeilijk is om op betrouwbare wijze multi-threaded gebruik te maken van de GPU (gemakkelijkere multi-threaded toegang tot de GPU is één motivatie voor het ontwikkelen van de nieuwe Vulkan- standaard ). Op Android WebView is er een aparte renderthread op besturingssysteemniveau voor tekenen, vanwege de manier waarop WebViews zijn ingebed in een native app. Andere platforms zullen in de toekomst waarschijnlijk zo'n draad hebben.

De weergavecompositor bevindt zich op een andere thread omdat deze te allen tijde moet reageren en geen enkele mogelijke bron van vertraging op de GPU-hoofdthread mag blokkeren. Eén oorzaak van de vertraging van de GPU-hoofdthread zijn oproepen naar niet-Chromium-code, zoals leverancierspecifieke GPU-stuurprogramma's, die op moeilijk te voorspellen manieren traag kunnen zijn.

Componentenstructuur

Binnen elke hoofd- of compositor-thread van het weergaveproces zijn er logische softwarecomponenten die op gestructureerde manieren met elkaar communiceren.

Render de hoofdthreadcomponenten van het proces

Een diagram van de Blink-renderer.

  • Knipperende renderer:
    • Het lokale frameboomfragment vertegenwoordigt de boom van lokale frames en de DOM binnen frames.
    • De component DOM en Canvas API's bevat implementaties van al deze API's.
    • De documentlevenscyclusloper voert de renderingpijplijnstappen uit tot en met de commit-stap.
    • De component voor het testen en verzenden van invoergebeurtenissen voert hittests uit om erachter te komen welk DOM-element het doelwit is van een gebeurtenis, en voert de algoritmen voor het verzenden van invoergebeurtenissen en het standaardgedrag uit.
  • De planner en runner van de renderende gebeurtenislus beslissen wat er op de gebeurtenislus moet worden uitgevoerd en wanneer. Het plant de weergave in een ritme dat overeenkomt met de weergave van het apparaat.

Een diagram van de frameboom.

Lokale frameboomfragmenten zijn een beetje ingewikkeld om over na te denken. Bedenk dat een frameboom de hoofdpagina en de onderliggende iframes is, recursief. Een frame is lokaal voor een weergaveproces als het in dat proces wordt weergegeven, en anders bevindt het zich op afstand .

U kunt zich voorstellen dat u frames kleurt op basis van hun weergaveproces. In de voorgaande afbeelding zijn de groene cirkels allemaal frames in één renderproces; de oranje zijn in een seconde, en de blauwe in een derde.

Een lokaal frameboomfragment is een verbonden component van dezelfde kleur in een frameboom. Er zijn vier lokale framebomen in de afbeelding: twee voor site A, één voor site B en één voor site C. Elke lokale frameboom krijgt zijn eigen Blink-renderercomponent. De Blink-renderer van een lokale frameboom bevindt zich mogelijk niet in hetzelfde renderproces als andere lokale framebomen (dit wordt bepaald door de manier waarop renderprocessen worden geselecteerd, zoals eerder beschreven).

Geef de threadstructuur van de procescompositor weer

Een diagram dat de componenten van de renderprocescompositor toont.

De componenten van de renderprocescompositor omvatten:

  • Een gegevenshandler die een samengestelde lagenlijst, weergavelijsten en eigenschappenbomen bijhoudt.
  • Een levenscyclusloper die de animatie-, scroll-, composiet-, raster- en decoderings- en activeringsstappen van de weergavepijplijn uitvoert. (Houd er rekening mee dat animeren en scrollen zowel in de hoofdthread als in de compositor kunnen voorkomen.)
  • Een invoer- en hittesthandler voert invoerverwerking en hittests uit met de resolutie van samengestelde lagen, om te bepalen of scrollbewegingen kunnen worden uitgevoerd op de compositorthread, en op welke renderproceshittests zich moeten richten.

Een voorbeeld uit de praktijk

Laten we de architectuur nu concreet maken met een voorbeeld. In dit voorbeeld zijn er drie tabbladen:

Tabblad 1: foo.com

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe  id=two src="bar.com"></iframe>
</html>

Tabblad 2: bar.com

<html>
 …
</html>

Tab 3: baz.com html <html> … </html>

De proces-, thread- en componentenstructuur voor deze tabbladen ziet er als volgt uit:

Diagram van het proces voor de tabbladen.

Laten we nu een voorbeeld bekijken van elk van de vier hoofdtaken van weergave, die, zoals u zich wellicht herinnert, zijn:

  1. Render inhoud in pixels op het scherm.
  2. Animeer visuele effecten op de inhoud van de ene staat naar de andere.
  3. Scroll als reactie op invoer.
  4. Leid invoer efficiënt naar de juiste plaatsen, zodat ontwikkelaarsscripts en andere subsystemen kunnen reageren.

Om de gewijzigde DOM voor tabblad één weer te geven :

  1. Een ontwikkelaarsscript verandert de DOM in het weergaveproces voor foo.com.
  2. De Blink-renderer vertelt de compositor dat er een render nodig is.
  3. De compositor vertelt Viz dat er een render nodig is.
  4. Viz signaleert het begin van de render terug naar de compositor.
  5. De compositor stuurt het startsignaal door naar de Blink-renderer.
  6. De hoofdthreadgebeurtenislooprunner voert de levenscyclus van het document uit.
  7. De hoofdthread verzendt het resultaat naar de compositorthread.
  8. De compositor-gebeurtenislooprunner voert de compositing-levenscyclus uit.
  9. Alle rastertaken worden voor raster naar Viz verzonden (er zijn vaak meer dan één van deze taken).
  10. Viz rastert inhoud op de GPU.
  11. Viz bevestigt voltooiing van de rastertaak. Opmerking: Chromium wacht vaak niet tot het raster is voltooid, maar gebruikt in plaats daarvan iets dat een synchronisatietoken wordt genoemd en dat moet worden opgelost door rastertaken voordat stap 15 wordt uitgevoerd.
  12. Er wordt een compositorframe naar Viz gestuurd.
  13. Viz verzamelt de compositorframes voor het foo.com-weergaveproces, het bar.com iframe-weergaveproces en de browsergebruikersinterface.
  14. Viz plant een gelijkspel.
  15. Viz tekent het geaggregeerde compositorframe naar het scherm.

Een CSS-transformatieovergang op tabblad twee animeren :

  1. De compositor-thread voor het bar.com-weergaveproces tikt een animatie aan in de compositor-gebeurtenislus door de bestaande eigenschappenbomen te muteren. Hierdoor wordt de levenscyclus van de compositor opnieuw uitgevoerd. (Raster- en decodeertaken kunnen voorkomen, maar worden hier niet weergegeven.)
  2. Er wordt een compositorframe naar Viz gestuurd.
  3. Viz verzamelt de compositorframes voor het foo.com-weergaveproces, het bar.com-weergaveproces en de browsergebruikersinterface.
  4. Viz plant een gelijkspel.
  5. Viz tekent het geaggregeerde compositorframe naar het scherm.

Om door de webpagina te scrollen op tabblad drie:

  1. Een reeks input (muis, aanraking of toetsenbord) komt naar het browserproces.
  2. Elke gebeurtenis wordt doorgestuurd naar de renderprocescompositorthread van baz.com.
  3. De compositor bepaalt of de hoofdthread op de hoogte moet zijn van de gebeurtenis.
  4. De gebeurtenis wordt indien nodig naar de hoofdthread verzonden.
  5. De hoofdthread vuurt luisteraars voor input ​​( pointerdown , touchstar , pointermove , touchmove of wheel ) om te zien of luisteraars preventDefault voor de gebeurtenis zullen aanroepen.
  6. De hoofdthread retourneert of preventDefault naar de compositor is aangeroepen.
  7. Als dit niet het geval is, wordt de invoergebeurtenis teruggestuurd naar het browserproces.
  8. Het browserproces converteert het naar een scrollgebaar door het te combineren met andere recente gebeurtenissen.
  9. Het scrollgebaar wordt nogmaals naar de renderprocescompositorthread van baz.com gestuurd,
  10. De scroll wordt daar toegepast en de compositor-thread voor het bar.com-weergaveproces tikt een animatie aan in de compositor-gebeurtenislus. Hierdoor wordt de scroll-offset in de eigenschappenbomen gemuteerd en wordt de levenscyclus van de compositor opnieuw uitgevoerd. Het vertelt de hoofdthread ook om een scroll af te vuren (hier niet afgebeeld).
  11. Er wordt een compositorframe naar Viz gestuurd.
  12. Viz verzamelt de compositorframes voor het foo.com-weergaveproces, het bar.com-weergaveproces en de browsergebruikersinterface.
  13. Viz plant een gelijkspel.
  14. Viz tekent het geaggregeerde compositorframe naar het scherm.

Een click routeren naar een hyperlink in iframe #twee op tabblad één:

  1. Er komt een input (muis, aanraking of toetsenbord) naar het browserproces. Het voert een geschatte hittest uit om te bepalen of het iframe-weergaveproces van bar.com de klik moet ontvangen en deze daarheen stuurt.
  2. De compositor-thread voor bar.com stuurt de click naar de hoofdthread voor bar.com en plant een rendering-gebeurtenislustaak om deze te verwerken.
  3. De invoergebeurtenisprocessor voor de hoofdthreadhittests van bar.com om te bepalen op welk DOM-element in het iframe is geklikt, en vuurt een click af zodat scripts deze kunnen observeren. Als u geen preventDefault hoort, navigeert het naar de hyperlink.
  4. Bij het laden van de bestemmingspagina van de hyperlink wordt de nieuwe status weergegeven, met stappen die vergelijkbaar zijn met het bovenstaande voorbeeld van "geef gewijzigde DOM weer". (Deze daaropvolgende wijzigingen worden hier niet weergegeven.)

Conclusie

Oef, dat waren veel details. Zoals je kunt zien, is weergave in Chromium behoorlijk ingewikkeld! Het kan veel tijd kosten om alle stukjes te onthouden en te internaliseren, dus maak je geen zorgen als het overweldigend lijkt.

De belangrijkste conclusie is dat er een conceptueel eenvoudige weergavepijplijn bestaat, die door zorgvuldige modularisering en aandacht voor detail is opgesplitst in een aantal op zichzelf staande componenten. Deze componenten zijn vervolgens verdeeld over parallelle processen en threads om de schaalbare prestaties en uitbreidbaarheidsmogelijkheden te maximaliseren.

Elk van deze componenten speelt een cruciale rol bij het mogelijk maken van alle prestaties en functies die moderne webapps nodig hebben. Binnenkort zullen we diepgaande informatie over elk van hen publiceren, en de belangrijke rol die ze spelen.

Maar daarvoor zal ik ook uitleggen hoe de belangrijkste datastructuren die in dit bericht worden genoemd (degene die in het blauw zijn aangegeven aan de zijkanten van het renderingpijplijndiagram) net zo belangrijk zijn voor RenderingNG als codecomponenten.

Bedankt voor het lezen, en blijf op de hoogte!

Illustraties door Una Kravets.