Panoramica dell'architettura RenderingNG

Chris Harrelson
Chris Harrelson

In un post precedente, ho fornito una panoramica degli obiettivi dell'architettura RenderingNG e delle proprietà principali. Questo post spiega come sono configurati i componenti e come avviene il flusso della pipeline di rendering.

A partire dal livello più alto e da lì in dettaglio, le attività di rendering sono:

  1. Esegui il rendering dei contenuti in pixel sullo schermo.
  2. Animare gli effetti visivi sui contenuti da uno stato all'altro.
  3. Scorri nella risposta all'input.
  4. Indirizza l'input in modo efficiente alle posizioni giuste in modo che gli script degli sviluppatori e altri sottosistemi possano rispondere.

I contenuti da visualizzare sono una struttura ad albero di frame per ogni scheda del browser, oltre all'interfaccia utente del browser. Un flusso di eventi di input non elaborati da touchscreen, mouse, tastiere e altri dispositivi hardware.

Ogni frame include:

  • Stato DOM
  • CSS
  • Canvas
  • Risorse esterne, ad esempio immagini, video, caratteri e SVG

Un frame è un documento HTML con il relativo URL. Una pagina web caricata in una scheda del browser include un frame di primo livello, frame secondari per ogni iframe incluso nel documento di primo livello e i relativi discendenti iframe ricorsivi.

Un effetto visivo è un'operazione grafica applicata a una bitmap, come lo scorrimento, la trasformazione, il clip, il filtro, l'opacità o la fusione.

Componenti dell'architettura

In RenderingNG, queste attività sono suddivise logicamente in più fasi e componenti di codice. I componenti finiscono in vari processi, thread e sottocomponenti della CPU al loro interno. Ognuna di esse svolge un ruolo importante nel raggiungimento di affidabilità, prestazioni scalabili ed estensibilità per tutti i contenuti web.

Struttura della pipeline di rendering

Diagramma della pipeline di rendering come spiegato nel testo seguente.

Il rendering avviene in una pipeline con una serie di fasi e artefatti creati lungo il percorso. Ogni fase rappresenta il codice che esegue un'attività ben definita all'interno del rendering. Gli artefatti sono strutture di dati che sono input o output delle fasi; nel diagramma, gli input o gli output sono indicati con frecce.

In questo post del blog non verranno illustrati molto nel dettaglio gli artefatti; lo vedremo nel prossimo post: Strutture di dati chiave e relativi ruoli in RenderingNG.

Le fasi della pipeline

Nel diagramma precedente, le fasi sono annotate con colori che indicano in quale thread o processo vengono eseguiti:

  • Verde:il thread principale del processo di rendering
  • Giallo: compositore del processo di rendering
  • Arancione: processo

In alcuni casi possono essere eseguite in più posizioni, a seconda delle circostanze, ed è per questo che alcune hanno due colori.

Le fasi sono:

  1. Animazione: cambia gli stili calcolati e modifica gli alberi delle proprietà nel tempo in base a linee temporali dichiarative.
  2. Stile: applica CSS al DOM e crea stili calcolati.
  3. Layout: determina le dimensioni e la posizione degli elementi DOM sullo schermo e crea un albero dei frammenti immutabile.
  4. Pre-colorazione: strutture di proprietà di computing e invalidate tutti gli elenchi di display esistenti e i riquadri di texture della GPU, a seconda dei casi.
  5. Scorrimento: aggiorna l'offset di scorrimento dei documenti e degli elementi DOM scorrevoli modificando le strutture delle proprietà.
  6. Paint: calcola un elenco di visualizzazione che descrive come rasterizzare i riquadri di texture GPU dal DOM.
  7. Commit: copia le strutture delle proprietà e l'elenco di visualizzazione nel thread del compositor.
  8. Livelli: suddividi l'elenco visualizzato in un elenco di livelli composti per la rasterizzazione e l'animazione indipendenti.
  9. Raster, decodifica e colorazione dei worklet: trasforma rispettivamente elenchi di visualizzazione, immagini codificate e codice di worklet di color in riquadri di texture GPU.
  10. Attiva: crea un frame composito che rappresenta come disegnare e posizionare i riquadri GPU sullo schermo, insieme agli eventuali effetti visivi.
  11. Aggrega: combina i frame del compositore di tutti i frame del compositore visibili in un unico frame del compositor globale.
  12. Disegna: esegui il frame del compositore aggregato sulla GPU per creare pixel sullo schermo.

Le fasi della pipeline di rendering possono essere ignorate se non sono necessarie. Ad esempio, le animazioni di effetti visivi e lo scorrimento possono saltare il layout, predipingere e colorare. Per questo motivo, l'animazione e lo scorrimento sono contrassegnati da puntini gialli e verdi nel diagramma. Se per gli effetti visivi è possibile saltare layout, pre-paint e Paint, è possibile eseguirli interamente sul thread del compositor e saltare il thread principale.

Il rendering dell'UI del browser non è rappresentato direttamente qui, ma può essere considerato come una versione semplificata della stessa pipeline (infatti la sua implementazione condivide gran parte del codice). Il video (anche non raffigurato direttamente) di solito esegue il rendering tramite codice indipendente che decodifica i frame in riquadri di texture GPU, che vengono poi collegati ai frame del compositore e alla fase di disegno.

Processo e struttura dei thread

Processi della CPU

L'uso di più processi della CPU consente di ottenere l'isolamento delle prestazioni e della sicurezza tra i siti e dallo stato del browser, nonché la stabilità e la sicurezza dall'hardware GPU.

Diagramma delle varie parti dei processi della CPU

  • Il processo di rendering esegue il rendering, l'animazione, lo scorrimento e il routing degli input per una singola combinazione di sito e scheda. Esistono molti processi di rendering.
  • Il processo del browser esegue il rendering, l'animazione e il routing degli input per l'interfaccia utente del browser (tra cui barra degli URL, titoli delle schede e icone) e instrada tutti gli input rimanenti al processo di rendering appropriato. Esiste esattamente un processo del browser.
  • Il processo di visualizzazione aggrega la composizione di più processi di rendering oltre al processo del browser. rasterizza e disegna usando la GPU. Esiste esattamente un processo di visualizzazione.

Siti diversi finiscono sempre in processi di rendering differenti. In realtà, sempre su computer e quando possibile sui dispositivi mobili. Scriverò "sempre" di seguito, ma questo avvertimento vale per tutto il testo.)

In genere, più schede o finestre del browser dello stesso sito vengono visualizzate in processi di rendering diversi, a meno che le schede non siano correlate (una apertura dell'altra). Se la memoria è elevata sul computer, Chromium potrebbe inserire più schede dello stesso sito nello stesso processo di rendering anche se non correlate.

All'interno di un'unica scheda del browser, i frame di siti diversi si trovano sempre in processi di rendering diversi l'uno dall'altro, ma i frame dello stesso sito si trovano sempre nello stesso processo di rendering. Dal punto di vista del rendering, il vantaggio importante dei processi di rendering multipli è che iframe e schede tra siti ottengono l'isolamento delle prestazioni l'uno dall'altro. Inoltre, le origini possono attivare un isolamento ancora maggiore.

Esiste esattamente un processo di visualizzazione per tutto Chromium. Dopotutto, di solito c'è solo una GPU e uno schermo a cui attingere. Separare Viz nel proprio processo è utile per la stabilità a fronte di bug nei driver della GPU o nell'hardware. È anche utile per l'isolamento di sicurezza, importante per le API GPU come Vulkan. È importante anche per la sicurezza in generale.

Il browser può avere molte schede e finestre, ognuna delle quali ha dei pixel di interfaccia utente del browser da tracciare, potresti chiederti: perché c'è esattamente un processo del browser? Il motivo è che è attiva solo una di queste schede per volta; infatti, le schede del browser non visibili vengono per lo più disattivate e eliminano tutta la memoria GPU. Tuttavia, le complesse funzionalità di rendering dell'interfaccia utente dei browser vengono implementate sempre più spesso nei processi di rendering (noti come WebUI). Questo non è per motivi di isolamento delle prestazioni, ma per sfruttare la facilità d'uso del motore di rendering web di Chromium.

Sui dispositivi Android meno recenti, il processo di rendering e del browser viene condiviso quando viene utilizzato in una WebView (in genere questo non si applica a Chromium su Android, ma solo a WebView). Su WebView, anche il processo del browser viene condiviso con l'app di incorporamento e WebView ha un solo processo di rendering.

A volte può essere utile anche una procedura per decodificare i contenuti video protetti. Questo processo non è illustrato sopra.

Thread

I thread consentono di ottenere isolamento delle prestazioni e reattività nonostante attività lente, pipeline in contemporanea e buffering multiplo.

Un diagramma del processo di rendering come descritto nell'articolo.

  • Il thread principale esegue gli script, il loop degli eventi di rendering, il ciclo di vita del documento, il test degli hit, l'invio di eventi di script e l'analisi di HTML, CSS e altri formati di dati.
    • Gli aiutanti thread principali svolgono attività come la creazione di bitmap e BLOB di immagini che richiedono la codifica o la decodifica.
    • I web worker eseguono lo script e un loop di eventi di rendering per OffscreenCanvas.
  • Il thread compositor elabora gli eventi di input, esegue lo scorrimento e le animazioni dei contenuti web, calcola la stratificazione ottimale dei contenuti web e coordina le decodificazioni delle immagini, il disegno dei worklet e le attività raster.
    • Gli helper dei thread dei compositor coordinano le attività del raster Viz ed eseguono le attività di decodifica delle immagini, il disegno dei worklet e il raster di fallback.
  • Thread di output multimediali, demuxer o audio decodifica, elabora e sincronizza stream video e audio. (Ricorda che il video viene eseguito in parallelo con la pipeline di rendering principale).

La separazione dei thread principale da quella del compositore è fondamentale per l'isolamento delle prestazioni dell'animazione e dello scorrimento dal lavoro del thread principale.

È presente un solo thread principale per processo di rendering, anche se più schede o frame dello stesso sito potrebbero finire nello stesso processo. Tuttavia, le prestazioni sono isolate dal lavoro eseguito in varie API del browser. Ad esempio, la generazione di bitmap e BLOB di immagini nell'API Canvas viene eseguita in un thread helper thread principale.

Allo stesso modo, c'è un solo thread di compositor per processo di rendering. Di solito non è un problema che esiste solo una, perché tutte le operazioni davvero costose sul thread del compositore sono delegate ai thread worker del compositore o al processo di visualizzazione, e questa operazione può essere eseguita in parallelo con routing, scorrimento o animazione dell'input. I thread dei worker del compositore coordinano le attività eseguite nel processo di visualizzazione, ma l'accelerazione della GPU ovunque potrebbe non riuscire per motivi fuori dal controllo di Chromium, ad esempio bug del driver. In queste situazioni, il thread worker svolgerà il lavoro in modalità di riserva sulla CPU.

Il numero di thread worker del compositore dipende dalle funzionalità del dispositivo. Ad esempio, i computer in genere utilizzano più thread, in quanto hanno più core della CPU e hanno meno limiti di batteria rispetto ai dispositivi mobili. Questo è un esempio di scalabilità verticale e dello scale down.

È anche interessante notare che l'architettura di threading del processo di rendering è un'applicazione di tre diversi modelli di ottimizzazione:

  • Thread di supporto: invio di attività secondarie a lunga esecuzione a thread aggiuntivi, per mantenere il thread principale reattivo alle altre richieste che avvengono contemporaneamente. I thread principali dell'helper thread e del compositore sono buoni esempi di questa tecnica.
  • Buffering multiplo: visualizzazione dei contenuti di cui è stato eseguito il rendering in precedenza durante il rendering di nuovi contenuti, per nascondere la latenza del rendering. Il thread del compositore utilizza questa tecnica.
  • Parallelizzazione della pipeline: viene eseguita la pipeline di rendering in più posizioni contemporaneamente. In questo modo lo scorrimento e l'animazione possono essere veloci, anche se viene eseguito un aggiornamento del rendering del thread principale, perché lo scorrimento e l'animazione possono essere eseguiti in parallelo.

Processo del browser

Diagramma del processo del browser che mostra la relazione tra il thread di rendering e di composizione e l'helper di thread di rendering e composizione.

  • Il thread di rendering e di compositing risponde all'input nell'interfaccia utente del browser, indirizza altri input al processo di rendering corretto; definisce e colora l'interfaccia utente del browser.
  • Gli helper di thread per il rendering e la composizione eseguono attività di decodifica delle immagini e raster o decodifica di fallback.

Il rendering del processo del browser e il thread di compositing sono simili al codice e alla funzionalità di un processo di rendering, ad eccezione del fatto che il thread principale e il thread del compositore sono combinati in uno. In questo caso è necessario un solo thread perché non è necessario un isolamento delle prestazioni da attività di thread principali lunghe, poiché non ne esiste nessuno per progettazione.

Procedura di visualizzazione

Un diagramma che mostra che il processo di visualizzazione include il thread principale della GPU e il thread del compositor del display.

  • I raster del thread principale della GPU visualizzano elenchi e frame video in riquadri di texture GPU e disegna frame del compositore sullo schermo.
  • Il thread di composizioner del display aggrega e ottimizza la composizione di ciascun processo di rendering, oltre al processo del browser, in un unico frame di composizione per la presentazione sullo schermo.

Il raster e il disegno avvengono generalmente sullo stesso thread, poiché entrambi si basano sulle risorse GPU ed è difficile utilizzare in modo affidabile l'utilizzo multi-thread della GPU (un accesso multi-threading più semplice alla GPU è uno dei motivi per lo sviluppo del nuovo standard Vulkan). Su Android WebView è presente un thread di rendering a livello di sistema operativo separato per il disegno, a causa del modo in cui i componenti WebView sono incorporati in un'app nativa. Altre piattaforme probabilmente avranno questo thread in futuro.

Il compositor di display si trova su un thread diverso perché deve essere sempre reattivo e non bloccare alcuna fonte di rallentamento nel thread principale della GPU. Una causa del rallentamento nel thread principale della GPU sono le chiamate a codice non Chromium, ad esempio i driver GPU specifici del fornitore, che potrebbero essere lenti in modi difficili da prevedere.

Struttura dei componenti

All'interno di ogni thread principale o di compositore del processo di rendering ci sono componenti software logici che interagiscono tra loro in modo strutturato.

Visualizza i componenti principali dei thread del processo

Un diagramma del renderer Blink.

  • Renderer lampeggiante:
    • Il frammento dell'albero dei frame locali rappresenta l'albero dei frame locali e il DOM all'interno dei frame.
    • Il componente API DOM e Canvas contiene le implementazioni di tutte queste API.
    • L'esecutore del ciclo di vita del documento esegue i passaggi della pipeline di rendering fino al passaggio del commit incluso.
    • Il componente Hit test e invio degli eventi di input esegue test sugli hit per scoprire quale elemento DOM viene scelto come target da un evento ed esegue gli algoritmi di invio degli eventi di input e i comportamenti predefiniti.
  • Lo pianificatore e il runner del loop di eventi di rendering decide cosa eseguire sul loop di eventi e quando. Pianifica il rendering in modo che venga eseguito con una cadenza corrispondente a quella del display del dispositivo.

Un diagramma dell'albero dei frame.

I frammenti di un albero di frame locale sono un po' complicati da considerare. Ricorda che una struttura frame è la pagina principale e i relativi iframe figlio, in modo ricorsivo. Un frame è locale rispetto a un processo di rendering se viene visualizzato in questo processo, oppure è remote.

Puoi immaginare di colorare i fotogrammi in base al loro processo di rendering. Nell'immagine precedente, i cerchi verdi sono tutti i frame in un processo di rendering, quelli arancioni in un secondo e quello blu in un terzo.

Un frammento di albero di frame locali è un componente collegato dello stesso colore in un albero di frame. Nell'immagine sono presenti quattro frame frame locali: due per il sito A, uno per il sito B e uno per il sito C. Ogni albero di frame locali riceve il proprio componente del renderer Blink. Il renderer Blink di un albero di frame locali potrebbe trovarsi o meno nello stesso processo di rendering di altre strutture di frame locali (viene determinato dal modo in cui vengono selezionati i processi di rendering, come descritto in precedenza).

Visualizza la struttura dei thread del compositor del processo di rendering

Un diagramma che mostra i componenti del compositore del processo di rendering.

I componenti compositore del processo di rendering includono:

  • Un gestore dei dati che gestisce un elenco di livelli composti, elenchi di visualizzazione e strutture di proprietà.
  • Un dispositivo di scorrimento del ciclo di vita che esegue i passaggi di animazione, scorrimento, composito, raster e decodifica e attivazione della pipeline di rendering. Ricorda che l'animazione e lo scorrimento possono verificarsi sia nel thread principale sia nel compositor.
  • Un gestore di input e test di hit esegue l'elaborazione degli input e gli hit test alla risoluzione dei livelli compositi per determinare se è possibile eseguire gesti di scorrimento nel thread del compositore e quali test degli hit del processo di rendering dovrebbero scegliere come target.

Un esempio concreto

Ora rendiamo concreta l'architettura con un esempio. In questo esempio sono presenti tre schede:

Scheda 1: foo.com

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

Scheda 2: bar.com

<html>
 …
</html>

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

La struttura del processo, del thread e dei componenti per queste schede sarà simile a questa:

Diagramma del processo delle schede.

Esaminiamo ora un esempio per ciascuna delle quattro attività principali del rendering, che, come ricorderai, sono:

  1. Esegui il rendering dei contenuti in pixel sullo schermo.
  2. Anima gli effetti visivi sui contenuti da uno stato all'altro.
  3. Scorri nella risposta all'input.
  4. Indirizza l'input in modo efficiente nelle posizioni giuste in modo che gli script degli sviluppatori e altri sottosistemi possano rispondere.

Per eseguire il rendering del DOM modificato per la scheda uno:

  1. Uno script sviluppatore modifica il DOM nel processo di rendering per foo.com.
  2. Il renderer Blink indica al compositore che per eseguire il rendering è necessario eseguire un rendering.
  3. Il compositore comunica a Viz che deve essere eseguito un rendering.
  4. Viz segnala l'inizio del rendering al compositor.
  5. Il compositore inoltra il segnale di avvio al renderer Blink.
  6. L'esecutore del loop dell'evento del thread principale esegue il ciclo di vita del documento.
  7. Il thread principale invia il risultato al thread del compositor.
  8. L'esecutore del loop di eventi del compositore esegue il ciclo di vita della composizione.
  9. Tutte le attività raster vengono inviate a Viz per il raster (spesso ce ne sono più di una).
  10. Visualizza i contenuti raster sulla GPU.
  11. Viz conferma il completamento dell'attività raster. Nota: spesso Chromium non attende il completamento del raster e utilizza invece un elemento chiamato token di sincronizzazione che deve essere risolto mediante attività raster prima dell'esecuzione del passaggio 15.
  12. Un frame del compositore viene inviato a Viz.
  13. Viz aggrega i frame del compositore per il processo di rendering foo.com, il processo di rendering dell'iframe bar.com e l'interfaccia utente del browser.
  14. Viz programma un pareggio.
  15. Visualizza il frame del compositore aggregato sullo schermo.

Per animare una transizione di trasformazione CSS nella scheda due:

  1. Il thread del compositor per il processo di rendering di bar.com seleziona un'animazione nel relativo loop di eventi di composizione mutando le strutture delle proprietà esistenti. Verrà quindi eseguito nuovamente il ciclo di vita del compositore. (Possono essere presenti attività di Raster e decodifica, che però non sono mostrate qui).
  2. Un frame del compositore viene inviato a Viz.
  3. Viz aggrega i frame del compositore per il processo di rendering di foo.com, il processo di rendering di bar.com e l'interfaccia utente del browser.
  4. Viz programma un pareggio.
  5. Visualizza il frame del compositore aggregato sullo schermo.

Per scorrere la pagina web nella terza scheda:

  1. Nel processo del browser viene avviata una sequenza di eventi input (mouse, tocco o tastiera).
  2. Ogni evento viene instradato al thread di compositore del processo di rendering di baz.com.
  3. Il compositore determina se il thread principale deve essere a conoscenza dell'evento.
  4. L'evento viene inviato, se necessario, al thread principale.
  5. Il thread principale attiva i listener di eventi input (pointerdown, touchstar, pointermove, touchmove o wheel) per vedere se i listener chiameranno preventDefault sull'evento.
  6. Il thread principale restituisce se preventDefault è stato chiamato al compositore.
  7. In caso contrario, l'evento di input viene inviato nuovamente al processo del browser.
  8. Il processo del browser lo converte in un gesto di scorrimento combinandolo con altri eventi recenti.
  9. Il gesto di scorrimento viene inviato di nuovo al thread di composizione del processo di rendering di baz.com.
  10. Lo scorrimento viene applicato lì e il thread del compositor per il processo di rendering bar.com seleziona un'animazione nel loop dell'evento del compositor. In seguito modifica l'offset di scorrimento nelle strutture delle proprietà ed esegue nuovamente il ciclo di vita del compositore. Comunica inoltre al thread principale di attivare un evento scroll (non illustrato qui).
  11. Un frame del compositore viene inviato a Viz.
  12. Viz aggrega i frame del compositore per il processo di rendering di foo.com, il processo di rendering di bar.com e l'interfaccia utente del browser.
  13. Viz programma un pareggio.
  14. Visualizza il frame del compositore aggregato sullo schermo.

Per indirizzare un evento click a un link ipertestuale nell'iframe #due nella scheda uno:

  1. Un evento input (mouse, tocco o tastiera) entra nel processo del browser. Esegue un hit test approssimativo per determinare che il processo di rendering dell'iframe bar.com deve ricevere il clic e lo invia lì.
  2. Il thread del compositor per bar.com instrada l'evento click al thread principale di bar.com e pianifica un'attività di loop di eventi di rendering per elaborarlo.
  3. Il processore di eventi di input per i test di hit dei thread principali di bar.com al fine di determinare su quale elemento DOM dell'iframe è stato fatto clic e attiva un evento click affinché gli script possano osservare. Non sentendo preventDefault, passerà al link ipertestuale.
  4. Al caricamento della pagina di destinazione del link ipertestuale, viene eseguito il rendering del nuovo stato, con passaggi simili a quelli dell'esempio "Visualizza il DOM modificato" sopra. Queste modifiche successive non vengono illustrate qui.

Conclusione

Fiuuu, sono stati tanti i dettagli. Come puoi vedere, il rendering in Chromium è piuttosto complicato. Può richiedere molto tempo per ricordare e interiorizzare tutti i pezzi, quindi non preoccuparti se ti sembra un'impresa troppo impegnativa.

Il concetto più importante è che esiste una pipeline di rendering concettualmente semplice, che, grazie a un'attenta modularizzazione e all'attenzione ai dettagli, è stata suddivisa in una serie di componenti autonomi. Questi componenti sono stati poi suddivisi tra processi e thread paralleli per massimizzare le opportunità di prestazioni scalabili ed estensibilità.

Ognuno di questi componenti è fondamentale per garantire tutte le prestazioni e le funzionalità necessarie per le app web moderne. Presto pubblicheremo approfondimenti su ognuno di questi e sui ruoli che rivestono.

Ma prima spiegherò anche come le strutture di dati chiave menzionate in questo post (quelle indicate in blu ai lati del diagramma della pipeline di rendering) sono importanti per RenderingNG quanto i componenti di codice.

Grazie per l'attenzione e continua a seguirci.

Illustrazioni di Una Kravets.