Approfondimento su RenderingNG: BlinkNG

Stefan Zager
Stefan Zager
Andrea Rossi
Chris Harrelson

Blink si riferisce all'implementazione della piattaforma web in Chromium e comprende tutte le fasi del rendering prima della composizione, fino al compositor commit. Puoi scoprire di più sull'architettura di rendering blink in un articolo precedente di questa serie.

Blink è nato come forchetta di WebKit, che a sua volta è una forchetta di KHTML, che risale al 1998. Contiene parte del codice più antico (e più critico) di Chromium e nel 2014 ne mostrava decisamente la data. In quell'anno, ci siamo imbarcati in una serie di progetti ambiziosi all'insegna di quello che chiamiamo BlinkNG, con l'obiettivo di colmare le lacune esistenti nell'organizzazione e nella struttura del codice Blink. Questo articolo esplorerà BlinkNG e i suoi progetti che lo compongono: perché li abbiamo realizzati, quali risultati hanno raggiunto, i principi guida su cui si basa il loro design e le opportunità di miglioramento che possono offrire in futuro.

La pipeline di rendering prima e dopo BlinkNG.

Rendering pre-NG

La pipeline di rendering all'interno di Blink era sempre concettualmente suddivisa in più fasi (style, layout, paint e così via), ma le barriere di astrazione erano del tutto inefficaci. In generale, i dati associati al rendering erano costituiti da oggetti modificabili di lunga durata. Questi oggetti potevano essere (e venivano) modificati in qualsiasi momento e venivano spesso riciclati e riutilizzati tramite successivi aggiornamenti del rendering. Era impossibile rispondere in modo affidabile a domande semplici come:

  • L'output di stile, layout o disegno deve essere aggiornato?
  • Quando riceveranno il valore "finale" per questi dati?
  • Quando è possibile modificare questi dati?
  • Quando verrà eliminato questo oggetto?

Ne esistono molti esempi, tra cui:

Stile genera ComputedStyle in base ai fogli di stile, ma ComputedStyle non era immutabile; in alcuni casi veniva modificato dalle fasi successive della pipeline.

Stile genera una struttura ad albero di LayoutObject, quindi layout annotava gli oggetti con informazioni sulle dimensioni e sul posizionamento. In alcuni casi, layout modifica persino la struttura ad albero. Non esisteva una chiara separazione tra gli input e gli output del layout.

Lo stile genera strutture di dati accessorie che determinano il corso della compositing, modificate da ogni fase dopo lo stile.

A un livello inferiore, i tipi di dati di rendering sono in gran parte costituiti da alberi specializzati (ad esempio, albero DOM, albero dello stile, albero del layout, albero delle proprietà di verniciatura); le fasi di rendering vengono implementate come camminate ricorrenti degli alberi. Idealmente, il percorso verso l'albero dovrebbe essere contenuto: durante l'elaborazione di un determinato nodo dell'albero, non dovremmo accedere a nessuna informazione esterna al sottoalbero radicato in quel nodo. Questo non è mai stato un vero pre-RenderingNG; il percorso per le informazioni a cui si accede spesso dai predecessori del nodo in fase di elaborazione. Ciò rende il sistema molto fragile e soggetto a errori. Era anche impossibile iniziare una camminata sugli alberi da qualsiasi parte, se non dalla radice.

Infine, c'erano molte ram nella pipeline di rendering distribuite nell'intero codice: layout forzati attivati da JavaScript, aggiornamenti parziali attivati durante il caricamento dei documenti, aggiornamenti forzati per preparare il targeting per eventi, aggiornamenti pianificati richiesti dal sistema di visualizzazione e API specializzate esposte solo al codice di test, per citarne alcuni. Nella pipeline di rendering erano presenti anche alcuni percorsi ricorrenti e rientranti, ovvero saltando all'inizio di una fase partendo da un'altra. Ognuna di queste rampe aveva il proprio comportamento idiosincratico e, in alcuni casi, l'output del rendering dipendeva dal modo in cui è stato attivato l'aggiornamento del rendering.

Cosa abbiamo cambiato

BlinkNG è composto da molti sottoprogetti, grandi e piccoli, con l'obiettivo comune di eliminare i deficit architettonici descritti in precedenza. Questi progetti condividono alcuni principi guida volti a rendere la pipeline di rendering più simile a una vera e propria pipeline:

  • Punto di ingresso uniforme: dobbiamo sempre entrare nella pipeline all'inizio.
  • Fasi funzionali: ogni fase deve avere input e output ben definiti e il suo comportamento deve essere funzionale, ovvero deterministico e ripetibile, e gli output devono dipendere solo dagli input definiti.
  • Input costanti: gli input di qualsiasi fase devono essere costantemente costanti mentre la fase è in esecuzione.
  • Output immutabili: una volta terminata una fase, i relativi output dovrebbero essere immutabili per il resto dell'aggiornamento del rendering.
  • Coerenza dei checkpoint: al termine di ogni fase, i dati di rendering prodotti finora dovrebbero trovarsi in uno stato coerente.
  • Deduplicazione del lavoro: calcola ogni elemento una sola volta.

Un elenco completo dei sottoprogetti di BlinkNG sarebbe noioso da leggere, ma ecco alcune delle conseguenze che seguono.

Il ciclo di vita del documento

La classe DocumentLifecycle tiene traccia dei nostri progressi nella pipeline di rendering. Ci consente di effettuare controlli di base che applicano gli invarianti elencati in precedenza, come ad esempio:

  • Se stiamo modificando una proprietà ComputedStyle, il ciclo di vita del documento deve essere kInStyleRecalc.
  • Se lo stato del ciclo di vita del documento è kStyleClean o successivo, NeedsStyleRecalc() deve restituire false per qualsiasi nodo collegato.
  • Quando entri nella fase del ciclo di vita del paint, lo stato del ciclo di vita deve essere kPrePaintClean.

Durante l'implementazione di BlinkNG, abbiamo eliminato sistematicamente i percorsi di codice che violavano questi elementi invarianti e abbiamo distribuito molte altre asserzioni in tutto il codice per evitare di regredire.

Se ti è mai capitato di essere nella tana del coniglio a guardare il codice di rendering di basso livello, potresti chiederti: "Come sono arrivato qui?". Come accennato in precedenza, la pipeline di rendering presenta una serie di punti di ingresso. In precedenza, ciò includeva percorsi di chiamata ricorrenti e di nuova entrata e i punti in cui siamo entrati nella pipeline in una fase intermedia, anziché iniziare dall'inizio. Nel corso di BlinkNG, abbiamo analizzato questi percorsi di chiamata e abbiamo stabilito che erano tutti riducibili a due scenari di base:

  • Tutti i dati sul rendering devono essere aggiornati, ad esempio, durante la generazione di nuovi pixel per la visualizzazione o l'esecuzione di un hit test per il targeting per eventi.
  • Abbiamo bisogno di un valore aggiornato per una query specifica a cui è possibile rispondere senza aggiornare tutti i dati di rendering. Include la maggior parte delle query JavaScript, ad esempio node.offsetTop.

Ora esistono solo due punti di ingresso nella pipeline di rendering, corrispondenti a questi due scenari. I percorsi del codice di nuovo accesso sono stati rimossi o sottoposti a refactoring e non è più possibile accedere alla pipeline iniziando da una fase intermedia. In questo modo è stato eliminato moltissimo mistero su quando e come avvengono gli aggiornamenti del rendering, rendendo molto più facile ragionare sul comportamento del sistema.

Stile delle tubature, layout e preverniciatura

Collettivamente, le fasi di rendering prima del paint sono responsabili di quanto segue:

  • Eseguire l'algoritmo style cascade per calcolare le proprietà di stile finali per i nodi DOM.
  • Generazione della struttura ad albero di layout che rappresenta la gerarchia a caselle del documento.
  • La determinazione delle informazioni su dimensioni e posizione per tutte le caselle.
  • Arrotondamento o agganciamento della geometria di sottopixel ai limiti di interi pixel per la colorazione.
  • Determinare le proprietà dei livelli compositi (trasformazione affine, filtri, opacità o qualsiasi altra cosa che possa essere accelerata GPU).
  • Individuazione dei contenuti che sono cambiati dalla fase di colorazione precedente e che devono essere dipinti o ridipinti (invalidazione di paint).

Questo elenco non è cambiato, ma prima di BlinkNG gran parte del lavoro veniva svolto in modo ad hoc, distribuito in più fasi di rendering, con molte funzionalità duplicate e inefficienze integrate. Ad esempio, la fase style è sempre stata la responsabilità principale del calcolo delle proprietà di stile finali per i nodi, ma in alcuni casi particolari i valori finali delle proprietà di stile non venivano determinati prima del completamento della fase style. Non c'era un punto formale o applicabile nel processo di rendering in cui potevamo dire con certezza che le informazioni di stile erano complete e immutabili.

Un altro buon esempio di problema pre-BlinkNG è l'annullamento della convalida di Paint. In precedenza, l'annullamento della convalida della colorazione si estendeva a tutte le fasi di rendering che portavano a questa fase. Durante la modifica del codice dello stile o del layout, era difficile sapere quali modifiche alla logica di annullamento dell'annullamento del disegno erano necessarie ed era facile commettere un errore che causava bug di invalidazione eccessiva o insufficiente. Puoi leggere ulteriori informazioni sulle complessità del vecchio sistema di invalidazione della vernice nell'articolo di questa serie dedicata a LayoutNG.

L'agganciamento della geometria del layout di sottopixel ai confini interi dei pixel per la pittura è un esempio di quando abbiamo avuto più implementazioni della stessa funzionalità e abbiamo fatto molto lavoro ridondante. Era presente un percorso codice di acquisizione pixel utilizzato dal sistema di disegno e un percorso di codice completamente separato usato ogni volta che fosse necessario un calcolo una tantum e in tempo reale delle coordinate di quest'ultimo al di fuori del codice di colorazione. Inutile dire che ogni implementazione aveva dei bug e che i risultati non corrispondevano sempre. Poiché non veniva eseguita la memorizzazione nella cache di queste informazioni, il sistema a volte eseguiva lo stesso calcolo ripetutamente, un ulteriore sovraccarico delle prestazioni.

Ecco alcuni progetti significativi che hanno eliminato i deficit architettonici delle fasi di rendering prima della colorazione.

Project Squad: sviluppare la fase dello stile

Questo progetto ha affrontato due principali deficit nella fase di stile che ne ha impedito una pipeline pulita:

Esistono due output principali della fase dello stile: ComputedStyle, che contengono il risultato dell'esecuzione dell'algoritmo CSS a cascata sull'albero DOM, e un albero di LayoutObjects, che stabilisce l'ordine delle operazioni per la fase del layout. Concettualmente, l'esecuzione dell'algoritmo a cascata dovrebbe avvenire rigorosamente prima di generare l'albero del layout, ma in precedenza queste due operazioni erano interleaving. Project Squad è riuscita a suddividere queste due fasi in fasi sequenziali distinte.

In precedenza, ComputedStyle non riceveva sempre il valore finale durante il ricalcolo dello stile; si verificavano alcune situazioni in cui ComputedStyle veniva aggiornato durante una fase successiva della pipeline. Project Squad ha eseguito correttamente il refactoring di questi percorsi di codice, in modo che ComputedStyle non venga mai modificato dopo la fase di stile.

LayoutNG: pianificazione della fase del layout

Questo progetto considerevole, uno dei pilastri di RenderingNG, è stata una riscrittura completa della fase di rendering del layout. Non renderemo giustizia all'intero progetto qui, ma ci sono alcuni aspetti degni di nota relativi al progetto BlinkNG nel suo complesso:

  • In precedenza, la fase del layout riceveva un albero di LayoutObject creato dalla fase di stile e veniva annotato l'albero con informazioni su dimensioni e posizione. Pertanto, non c'era una separazione chiara degli input dagli output. LayoutNG ha introdotto l'albero dei frammenti, che è l'output principale di sola lettura del layout e che funge da input principale per le fasi di rendering successive.
  • LayoutNG ha aggiunto la proprietà di contenimento al layout: quando calcoli le dimensioni e la posizione di un determinato LayoutObject, non guardiamo più al di fuori del sottoalbero radicato in quell'oggetto. Tutte le informazioni necessarie per aggiornare il layout di un determinato oggetto vengono calcolate preventivamente e fornite all'algoritmo come input di sola lettura.
  • In precedenza, si sono verificati casi limite in cui l'algoritmo del layout non funzionava strettamente: il risultato dipendeva dall'aggiornamento precedente del layout più recente. LayoutNG ha eliminato questi casi.

La fase di pre-colorazione

In precedenza, non esisteva una fase di rendering formale del pre-paint, ma solo una serie di operazioni di post-layout. La fase di pre-paint è cresciuta poiché erano presenti alcune funzioni correlate che potevano essere implementate al meglio come attraversamento sistematico dell'albero del layout dopo il completamento del layout; soprattutto:

  • Invio di invalidazioni di Paint: è molto difficile eseguire correttamente questa operazione durante il layout, quando le informazioni sono incomplete. È molto più facile fare la scelta giusta e può risultare molto efficiente se è suddiviso in due processi distinti: durante lo stile e il layout, i contenuti possono essere contrassegnati con un semplice flag booleano che potrebbe richiedere l'annullamento della convalida di Paint. Durante il percorso di pre-colorazione, controlliamo queste segnalazioni e, se necessario, emettiamo invalidazioni.
  • Generazione di alberi di proprietà di colorazione: un processo descritto in maggiore dettaglio più avanti.
  • Calcolo e registrazione delle posizioni di disegno scattate con pixel: i risultati registrati possono essere utilizzati dalla fase di colorazione e anche da qualsiasi codice downstream che ne abbia bisogno, senza alcun calcolo ridondante.

Alberi della proprietà: geometria coerente

Gli alberi delle proprietà sono stati introdotti all'inizio di RenderingNG per gestire la complessità dello scorrimento, che sul web ha una struttura diversa rispetto a tutti gli altri tipi di effetti visivi. Prima degli alberi delle proprietà, il compositore di Chromium utilizzava un'unica gerarchia di "livelli" per rappresentare la relazione geometrica dei contenuti compositi, ma questa si dissolveva rapidamente a causa delle complessità di elementi quali position:fixed. Nella gerarchia dei livelli si crescevano i puntatori non locali aggiuntivi che indicavano il "principale scorrimento" o "principale del clip" di un livello e in breve tempo è stato molto difficile comprendere il codice.

Le strutture delle proprietà hanno risolto il problema rappresentando gli aspetti di scorrimento e clip dell'overflow dei contenuti separatamente da tutti gli altri effetti visivi. In questo modo è stato possibile modellare correttamente la vera struttura visiva e a scorrimento dei siti web. Dopodiché dovevamo soltanto implementare algoritmi sopra gli alberi delle proprietà, come la trasformazione dello spazio sullo schermo dei livelli compositi o determinare quali livelli facevano scorrere e quali no.

Infatti, abbiamo presto notato che nel codice c'erano molti altri punti in cui venivano sollevate simili domande geometriche. Il post sulle strutture di dati chiave contiene un elenco più completo. Molti di loro presentavano implementazioni duplicate della stessa cosa che stava facendo il codice del compositore; tutti avevano un sottoinsieme diverso di bug e nessuno di loro era modellato correttamente sulla vera struttura del sito web. La soluzione è diventata chiara: centralizza tutti gli algoritmi geometrici in un unico posto e rifattorizza tutto il codice per utilizzarlo.

A loro volta, questi algoritmi dipendono tutti da alberi delle proprietà, motivo per cui queste sono una struttura di dati chiave, ovvero utilizzata in tutta la pipeline di RenderingNG. Quindi, per raggiungere questo obiettivo di codice geometrico centralizzato, dovevamo introdurre il concetto di alberi di proprietà molto prima nella pipeline, nella fase di pre-colorazione, e cambiare tutte le API che ora dipendevano da queste per richiedere l'esecuzione della pre-colorazione prima dell'esecuzione.

Questa storia è l'ennesimo aspetto del modello di refactoring di BlinkNG: identificare i calcoli chiave, effettuare il refactoring per evitare di duplicarli e creare fasi di pipeline ben definite che creano le strutture di dati che li alimentano. Calcoliamo gli alberi delle proprietà esattamente nel momento in cui sono disponibili tutte le informazioni necessarie e ci assicuriamo che gli alberi delle proprietà non possano cambiare durante le successive fasi di rendering.

Composito dopo la verniciatura: vernice e compositing per tubi

La stratificazione è il processo che consente di capire quali contenuti DOM inserire nel proprio livello composito (che, a sua volta, rappresenta una texture GPU). Prima di RenderingNG, la stratificazione veniva eseguita prima del disegno, non dopo (vedi qui la pipeline attuale: tieni presente il cambio di ordine). Prima dovevamo decidere in quali parti del DOM inserire il livello composito e poi disegnare elenchi di visualizzazione per quelle texture. Naturalmente, le decisioni dipendevano da fattori quali gli elementi DOM che animavano o scorrevano o avevano trasformazioni 3D e quali elementi sovrapposti.

Questo ha causato gravi problemi, perché richiedeva più o meno la presenza di dipendenze circolari nel codice, il che rappresenta un grosso problema per una pipeline di rendering. Vediamo il perché attraverso un esempio. Supponiamo di dover invalidate la convalida di Paint, ovvero di dover disegnare di nuovo l'elenco di elementi da visualizzare e poi rasternarlo. La necessità di invalidare potrebbe essere dovuta a una modifica nel DOM oppure a una modifica dello stile o del layout. Ovviamente, vogliamo invalidare soltanto le parti che sono state effettivamente modificate. Ciò significava scoprire quali livelli compositi erano interessati e quindi invalidare parzialmente o tutti gli elenchi visualizzati per quei livelli.

Ciò significa che l'annullamento della convalida dipendeva da DOM, stile, layout e decisioni di stratificazione dei livelli precedenti (passato: significato per il frame visualizzato precedente). Ma l'attuale stratizzazione dipende anche da tutti questi fattori. Inoltre, poiché non avevamo due copie di tutti i dati di stratificazione, era difficile distinguere le decisioni di stratificazione passate e quelle future. Abbiamo quindi finito con molto codice con un ragionamento circolare. Ciò causava a volte un codice illogico o errato, o persino arresti anomali o problemi di sicurezza, se non stavamo facendo molta attenzione.

Per far fronte a questa situazione, all'inizio abbiamo introdotto il concetto dell'oggetto DisableCompositingQueryAsserts. La maggior parte delle volte, se il codice tentasse di interrogare decisioni di stratificazione precedenti, causa un errore di asserzione e l'arresto anomalo del browser se era in modalità di debug. Questo ci ha aiutato a evitare di introdurre nuovi bug. E in ogni caso in cui il codice necessario legittimamente per interrogare le precedenti decisioni di stratificazione, inseriamo il codice per consentirne l'allocazione allocando un oggetto DisableCompositingQueryAsserts.

Il nostro piano era, nel tempo, eliminare tutti gli oggetti DisableCompositingQueryAssert dei siti di chiamata e quindi dichiarare il codice sicuro e corretto. Tuttavia, abbiamo scoperto che alcune chiamate erano essenzialmente impossibili da rimuovere, purché la stratificazione avvenisse prima della colorazione. (Finalmente siamo riusciti a rimuoverlo solo molto di recente). Questo è stato il primo motivo scoperto per il progetto Composite After Paint. Ciò che abbiamo imparato è che, anche se la fase della pipeline è ben definita per un'operazione, se si trova nel punto sbagliato della pipeline alla fine rimarrai bloccato.

Il secondo motivo alla base del progetto Composite After Paint è stato il bug Fundamental Compositing. Un modo per affermare questo bug è che gli elementi DOM non sono una buona rappresentazione 1:1 di uno schema di stratificazione efficiente o completo per i contenuti delle pagine web. Inoltre, poiché la composizione era prima della colorazione, dipendeva più o meno intrinsecamente da elementi DOM, non da visualizzare elenchi o alberi di proprietà. Questo è molto simile al motivo per cui abbiamo introdotto gli alberi di proprietà e, proprio come per gli alberi di proprietà, la soluzione cade direttamente se capisci la fase giusta della pipeline, la esegui al momento giusto e le fornisci le strutture di dati chiave corrette. E come per gli alberi di proprietà, questa era una buona opportunità per garantire che, una volta completata la fase di verniciatura, la sua produzione risulta immutabile per tutte le fasi successive della pipeline.

Vantaggi

Come hai visto, una pipeline di rendering ben definita offre enormi vantaggi a lungo termine. Ci sono anche più di quanto tu possa pensare:

  • Affidabilità notevolmente migliorata: questo aspetto è piuttosto immediato. Un codice più chiaro con interfacce ben definite e comprensibili è più facile da capire, scrivere e testare. Ciò la rende più affidabile. Inoltre, rende il codice più sicuro e stabile, con meno arresti anomali e meno bug "use-dopo-free".
  • Copertura dei test estesa: nel corso di BlinkNG, abbiamo aggiunto molti nuovi test alla nostra suite. Sono inclusi test delle unità che forniscono verifiche mirate degli elementi interni, test di regressione che ci impediscono di reintrodurre vecchi bug che abbiamo corretto (moltissimi!) e molte aggiunte alla Web Platform Test Suite gestite collettivamente, che vengono utilizzate da tutti i browser per misurare la conformità agli standard web.
  • Più facile da estendere: se un sistema è suddiviso in componenti chiari, non è necessario comprendere altri componenti a nessun livello di dettaglio per fare progressi su quello attuale. In questo modo è più facile per tutti aggiungere valore al codice di rendering senza dover essere un esperto e inoltre è più facile ragionare sul comportamento dell'intero sistema.
  • Prestazioni: ottimizzare gli algoritmi scritti in codice spaghetti è già abbastanza difficile, ma è quasi impossibile realizzare cose ancora più grandi come le animazioni e lo scorrimento universali in thread o i processi e i thread per l'isolamento dei siti senza una pipeline di questo tipo. Il parallelismo può aiutarci a migliorare enormemente il rendimento, ma è anche estremamente complicato.
  • Rendimento e contenimento: ci sono diverse nuove funzionalità rese possibili da BlinkNG che esercitano la pipeline in modi nuovi e inediti. Ad esempio, cosa succede se volessimo eseguire la pipeline di rendering solo fino alla scadenza di un budget? Oppure saltare il rendering per i sottoalberi che al momento risultano non pertinenti per l'utente? È questa la funzionalità attivata dalla proprietà CSS content-visibility. Puoi scegliere invece di far dipendere lo stile di un componente dal suo layout? Sono le query container.

Case study: query container

Le query container sono una funzionalità della piattaforma web imminente molto attesi (è da anni la funzionalità numero uno più richiesta dagli sviluppatori di CSS). Se è così bello, perché non esiste ancora? Il motivo è che l'implementazione delle query del container richiede una comprensione e un controllo molto accurati della relazione tra il codice di stile e layout. Diamo un'occhiata in dettaglio.

Una query container consente agli stili applicati a un elemento di dipendere dalle dimensioni disposte di un predecessore. Poiché le dimensioni del layout vengono calcolate durante il layout, dobbiamo eseguire il ricalcolo dello stile dopo il layout, ma il ricalcolo stile viene eseguito prima del layout. Questo paradosso dell'uovo e della gallina è l'intero motivo per cui non è stato possibile implementare query container prima di BlinkNG.

Come possiamo risolvere il problema? Non è una dipendenza retrocessa dalla pipeline, ovvero lo stesso problema risolto con progetti come Composite After Paint? Peggio ancora, cosa succede se i nuovi stili cambiano la dimensione del predecessore? Questo a volte non produce un ciclo infinito?

In linea di principio, la dipendenza circolare può essere risolta utilizzando la proprietà CSS "contiene", che consente il rendering all'esterno di un elemento non dipende dalla visualizzazione nel sottoalbero dell'elemento in questione. Ciò significa che i nuovi stili applicati da un contenitore non possono influire sulle dimensioni del contenitore, perché le query relative al contenitore richiedono il contenimento.

Ma in realtà, ciò non era sufficiente, e si rendeva necessario introdurre un tipo di contenimento più debole del semplice contenimento dimensionale. Questo perché è comune che un contenitore delle query contenitore possa essere ridimensionato in una sola direzione (di solito, blocco) in base alle sue dimensioni in linea. Quindi è stato aggiunto il concetto di contenimento delle dimensioni in linea. Ma, come puoi vedere dalla nota molto lunga in quella sezione, per molto tempo non era del tutto chiaro se fosse possibile il contenimento della dimensione in linea.

Una cosa è descrivere il contenimento in un linguaggio spec astratto, mentre un'altra è implementarlo correttamente. Ricordiamo che uno degli obiettivi di BlinkNG era quello di portare il principio di contenimento nei percorsi che costituiscono la logica principale del rendering: quando si attraversa un sottoalbero, non dovrebbero essere richieste informazioni dall'esterno del sottoalbero. In quel momento (beh, non è stato esattamente un incidente), è molto più pulito e facile da implementare il contenimento CSS se il codice di rendering rispetta il principio di contenimento.

Futuro: compositing fuori dal thread... e oltre!

La pipeline di rendering mostrata qui è in realtà un po' avanti rispetto all'attuale implementazione di RenderingNG. Mostra la suddivisione in livelli come esterna al thread principale, mentre al momento si trova ancora nel thread principale. Tuttavia, è solo una questione di tempo prima che questo venga fatto, ora che Composite After Paint è stato spedito e la stratificazione è dopo la colorazione.

Per capire perché è importante e dove altro potrebbe portare, dobbiamo considerare l'architettura del motore di rendering da una prospettiva un po' più vista. Uno degli ostacoli più duraturi per migliorare le prestazioni di Chromium è il semplice fatto che il thread principale del renderer gestisce sia la logica principale dell'applicazione (ovvero lo script in esecuzione) sia la maggior parte del rendering. Di conseguenza, il thread principale è spesso saturato di lavoro e la congestione del thread principale è spesso il collo di bottiglia dell'intero browser.

La buona notizia è che non deve essere necessariamente così. Questo aspetto dell'architettura di Chromium risale ai giorni di KHTML, quando l'esecuzione a thread unico era il modello di programmazione dominante. Quando i processori multi-core sono diventati comuni nei dispositivi di livello consumer, l'ipotesi a thread singolo è stata completamente integrata in Blink (in precedenza WebKit). Da molto tempo volevamo introdurre più thread nel motore di rendering, ma nel vecchio sistema era semplicemente impossibile. Uno degli obiettivi principali del rendering NG era quello di scavare in questa buca e rendere possibile spostare il lavoro di rendering, in parte o nel suo complesso, in un altro thread (o thread).

Ora che BlinkNG si sta avvicinando, stiamo già iniziando a esplorare quest'area. Il coming senza blocco è una prima incursione nella modifica del modello di threading del renderer. Il compositor commit (o semplicemente commit) è un passaggio di sincronizzazione tra il thread principale e il thread del compositor. Durante il commit, creiamo copie dei dati di rendering prodotti nel thread principale da usare dal codice di compositing downstream in esecuzione nel thread del compositor. Durante la sincronizzazione, l'esecuzione del thread principale viene interrotta mentre il codice di copia viene eseguito nel thread del compositor. Questo avviene per garantire che il thread principale non modifichi i dati di rendering mentre il thread del compositore li copia.

Il commit non bloccante eliminerà la necessità dell'arresto del thread principale e attenderà la fine della fase di commit: il thread principale continuerà a funzionare mentre il commit viene eseguito contemporaneamente sul thread del compositore. L'effetto netto del commit non bloccante sarà una riduzione del tempo dedicato al lavoro di rendering sul thread principale, il che diminuirà la congestione del thread principale e migliorerà le prestazioni. Alla data di stesura di questo documento (marzo 2022), abbiamo un prototipo funzionante di commit non di blocco e ci stiamo preparando a effettuare un'analisi dettagliata del suo impatto sulle prestazioni.

In attesa c'è la compositing off-main-thread, con l'obiettivo di far corrispondere il motore di rendering all'illustrazione spostando la stratificazione dal thread principale a un thread di lavoro. Come il commit non bloccante, questo riduce la congestione sul thread principale diminuendo il carico di lavoro di rendering. Un progetto come questo non sarebbe mai stato possibile senza i miglioramenti architettonici di Composite After Paint.

E ci sono altri progetti nella pipeline (si intende il gioco di parole). Finalmente abbiamo una base che consente di sperimentare con la ridistribuzione del lavoro di rendering e non vediamo l'ora di scoprire cosa sarà possibile.