Approfondimento su RenderingNG: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Blink si riferisce all'implementazione della piattaforma web da parte di Chromium e include tutte le fasi del rendering prima della composizione, che culminano con il commit del compositor. Puoi scoprire di più sull'architettura di rendering lampeggiante in un articolo precedente di questa serie.

Blink è nato come fork di WebKit, che a sua volta è una forchetta di KHTML, risalente al 1998. Contiene alcuni dei codici più vecchi (e più importanti) di Chromium e nel 2014 ha definitivamente mostrato la sua età. In quell'anno, abbiamo avviato una serie di progetti ambiziosi che si fondano su quello che chiamiamo BlinkNG, con l'obiettivo di risolvere carenze di lunga data nell'organizzazione e nella struttura del codice Blink. In questo articolo parleremo dei progetti di BlinkNG e dei suoi membri: perché li abbiamo realizzati, cosa hanno realizzato, i principi guida che hanno plasmato il loro design e le opportunità di miglioramenti futuri che possono offrire.

La pipeline di rendering prima e dopo BlinkNG.

Rendering pre-NG

La pipeline di rendering all'interno di Blink era sempre concettualmente suddivisa in fasi (stile, layout, colorazione e così via), ma le barriere di astrazione erano fuoriuscite. In generale, i dati associati al rendering consistevano in oggetti mutevoli e di lunga durata. Questi oggetti potevano essere, e sono stati, modificati in qualsiasi momento e spesso sono stati riciclati e riutilizzati tramite successivi aggiornamenti del rendering. Era impossibile rispondere in modo affidabile a domande semplici quali:

  • L'output dello stile, del layout o del colore deve essere aggiornato?
  • Quando riceveranno questi dati la data "finale" ?
  • Quando è possibile modificare questi dati?
  • Quando verrà eliminato questo oggetto?

Ne sono disponibili molti esempi, tra cui:

Stile genererebbe ComputedStyle in base ai fogli di stile; ma ComputedStyle non era immutabile; in alcuni casi potrebbe essere modificato alle fasi successive della pipeline.

Lo Stile genera una struttura ad albero di LayoutObject, quindi layout annota questi oggetti con informazioni su dimensioni e 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 generava strutture dati accessorie che determinavano il corso della composizione, che venivano modificate in 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 degli stili, albero di layout, albero delle proprietà Paint). e di rendering sono implementate come percorsi ricorsivi ad albero. Idealmente, un'esplorazione ad albero dovrebbe essere contenuta: quando elaboriamo un determinato nodo ad albero, non dovremmo accedere ad alcuna informazione esterna al sottoalbero radicato in quel nodo. Questo non è mai stato vero prima del renderingNG; ad albero delle informazioni a cui si accede frequentemente dai predecessori del nodo in elaborazione. Ciò ha reso il sistema molto fragile e soggetto a errori. Inoltre, era impossibile iniziare a camminare su un albero se non dalla sua radice.

Infine, c'erano molte rampe di avvio nella pipeline di rendering disseminate in tutto il codice: layout forzati attivati da JavaScript, aggiornamenti parziali attivati durante il caricamento dei documenti, aggiornamenti forzati per la preparazione del targeting per eventi, aggiornamenti pianificati richiesti dal sistema di visualizzazione e API specializzate esposte solo al codice di test, per citarne alcuni. C'erano anche alcuni percorsi ricorsivi e rientranti nella pipeline di rendering (ovvero, il passaggio all'inizio di una fase dalla metà di un'altra). Ognuna di queste rampe di attivazione aveva il proprio comportamento idiosincrotico 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 pensati per rendere la pipeline di rendering più simile a una pipeline reale:

  • Punto di ingresso uniforme: dovremmo sempre entrare nella pipeline dall'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 dovrebbero dipendere solo dagli input definiti.
  • Input costanti: gli input di qualsiasi fase devono essere effettivamente costanti mentre la fase è in esecuzione.
  • Output immutabili: una volta terminata una fase, i suoi output dovrebbero essere immutabili per il resto dell'aggiornamento del rendering.
  • Coerenza del checkpoint: alla fine di ogni fase, i dati di rendering prodotti finora dovrebbero essere in uno stato autocoerente.
  • Deduplicazione del lavoro: calcola ogni cosa una sola volta.

Un elenco completo di progetti secondari di BlinkNG potrebbe essere noioso di lettura, ma quelli che seguono sono alcuni degli effetti particolarmente importanti.

Il ciclo di vita dei documenti

La classe DocumentLifecycle tiene traccia dei nostri progressi attraverso la pipeline di rendering. Ci consente di eseguire controlli di base che applicano le invarianti elencate in precedenza, ad esempio:

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

Durante l'implementazione di BlinkNG, abbiamo eliminato sistematicamente i percorsi di codice che violavano queste invarianti e aggiunto molte altre asserzioni nel codice per assicurarci di non regredire.

Se non ti è mai capitato di guardare un codice di rendering di basso livello, potresti chiederti: "Come sono arrivato qui?" Come accennato in precedenza, esistono vari punti di ingresso nella pipeline di rendering. In precedenza, questo includeva percorsi di chiamata ricorsivi e di ritorno e punti in cui siamo entrati nella pipeline in una fase intermedia, piuttosto che partire dall'inizio. Nel corso di BlinkNG, abbiamo analizzato questi percorsi di chiamata e abbiamo determinato che erano tutti riducibili a due scenari di base:

  • Tutti i dati di rendering devono essere aggiornati, ad esempio quando vengono generati nuovi pixel per la visualizzazione o quando si esegue un test hit per il targeting degli eventi.
  • Abbiamo bisogno di un valore aggiornato per una query specifica a cui è possibile rispondere senza aggiornare tutti i dati di rendering. È inclusa la maggior parte delle query JavaScript, ad esempio node.offsetTop.

Ora ci sono solo due punti di ingresso nella pipeline di rendering, corrispondenti a questi due scenari. I percorsi di codice rientrati sono stati rimossi o sottoposti a refactoring e non è più possibile entrare nella pipeline a partire da una fase intermedia. In questo modo è stato chiarito come e quando vengono eseguiti gli aggiornamenti del rendering, rendendo molto più facile ragionare sul comportamento del sistema.

Stile, layout e precolorazione della pipeline

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

  • Eseguire l'algoritmo style cascade per calcolare le proprietà finali degli stili per i nodi DOM.
  • È in corso la generazione della struttura di layout che rappresenta la gerarchia dei riquadri del documento.
  • Determinazione delle informazioni su dimensioni e posizione di tutte le scatole.
  • Arrotondamento o agganciamento della geometria di sottopixel a interi limiti di pixel per la pittura.
  • Determinare le proprietà degli strati compositi (trasformazione affine, filtri, opacità o qualsiasi altra cosa che può essere accelerata GPU).
  • Determinare quali contenuti sono stati modificati rispetto alla precedente fase di colorazione e che devono essere dipinti o ricolorati (annullamento della convalida della colorazione).

Questo elenco non è cambiato, ma prima di BlinkNG gran parte di questo 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 principale responsabile del calcolo delle proprietà di stile finali dei nodi, ma si sono verificati alcuni casi speciali in cui non veniva determinato i valori finali delle proprietà di stile fino al completamento della fase style. Non c'era un punto formale o applicabile nel processo di rendering in cui potremmo affermare con certezza che le informazioni sullo stile erano complete e immutabili.

Un altro buon esempio di problema pre-BlinkNG è l'invalidazione Paint. In precedenza, l'annullamento della convalida del codice vernice era disseminato in tutte le fasi di rendering che portavano a quella di colorazione. Durante la modifica del codice dello stile o del layout, era difficile sapere quali modifiche fossero necessarie per colorare la logica di invalidazione ed era facile commettere un errore che causava bug di invalidazione insufficiente o eccessiva. Per ulteriori informazioni sulle complessità del vecchio sistema di invalidazione delle vernice, consulta l'articolo di questa serie dedicata a LayoutNG.

L'aggancio della geometria di layout dei sottopixel a interi limiti di pixel per la pittura è un esempio di cui abbiamo implementato più implementazioni della stessa funzionalità e abbiamo svolto molto lavoro ridondante. C'era un percorso di codice per l'aggancio dei pixel utilizzato dal sistema di colorazione e un percorso di codice completamente separato utilizzato ogni volta che avevamo bisogno di un calcolo una tantum e in tempo reale delle coordinate agganciate ai pixel al di fuori del codice di colorazione. È ovvio che ogni implementazione presentava i propri bug e i risultati non sempre corrispondevano. Poiché queste informazioni non venivano memorizzate nella cache, il sistema a volte eseguiva lo stesso identico calcolo ripetutamente, mettendo a dura prova le prestazioni.

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

Project Squad: pipelinere la fase di stile

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

Sono presenti due output primari della fase di stile: ComputedStyle, contenenti il risultato dell'esecuzione dell'algoritmo cascade CSS sull'albero DOM; e una struttura ad albero di LayoutObjects, che stabilisce l'ordine delle operazioni per la fase di layout. Concettualmente, l'esecuzione dell'algoritmo cascade dovrebbe avvenire strettamente prima di generare l'albero del layout. ma in precedenza queste due operazioni erano interlacciate. Project Squad è riuscito a suddividere questi due elementi in fasi distinte e sequenziali.

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

LayoutNG: pipeline della fase di layout

Questo progetto monumento, uno dei pilastri di RenderingNG, era una riscrittura completa della fase di rendering del layout. Non renderemo giustizia all'intero progetto, ma ecco alcuni aspetti importanti del progetto BlinkNG:

  • In precedenza, la fase di layout riceveva un albero di LayoutObject creato dalla fase di stile e annotato l'albero con informazioni su dimensioni e posizione. Di conseguenza, non esisteva una separazione netta tra gli input e gli 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 introdotto la proprietà di contenimento nel layout: quando calcoliamo le dimensioni e la posizione di un determinato LayoutObject, non guardiamo più fuori dal sottoalbero radicato a quell'oggetto. Tutte le informazioni necessarie per aggiornare il layout di un determinato oggetto vengono calcolate in anticipo e fornite come input di sola lettura all'algoritmo.
  • In precedenza, si verificavano casi limite in cui l'algoritmo del layout non era strettamente funzionale: il risultato dell'algoritmo dipendeva dall'ultimo aggiornamento del layout precedente. LayoutNG ha eliminato questi casi.

La fase di pre-colorazione

In precedenza, non esisteva una fase formale di rendering pre-paint, ma solo una serie di operazioni post-layout. La fase di pre-paint si è sviluppata perché ci sono alcune funzioni correlate che potevano essere implementate al meglio come un attraversamento sistematico della struttura ad albero una volta completato il layout. cosa più importante:

  • Emissione di invalidazioni del colore: è molto difficile eseguire correttamente l'annullamento della convalida dei colori durante il layout se le informazioni sono incomplete. È molto più facile fare la cosa giusta e può essere molto efficiente se è divisa in due processi distinti: durante lo stile e il layout, i contenuti possono essere contrassegnati con un semplice flag booleano come "potrebbe richiedere l'annullamento della convalida del disegno". Durante la fase di pre-colorazione, controlliamo questi flag ed emettiamo invalidazioni, se necessario.
  • Generazione di alberi per la pittura di proprietà: una procedura descritta in modo più dettagliato più avanti.
  • Calcolo e registrazione delle posizioni di disegno con pixel agganciate: i risultati registrati possono essere utilizzati dalla fase di colorazione e anche da qualsiasi codice downstream che ne ha bisogno, senza calcoli ridondanti.

Alberi delle proprietà: geometria coerente

Gli alberi di 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 ha utilizzato un singolo "livello" della gerarchia per rappresentare la relazione geometrica dei contenuti compositi, che poi sono rapidamente andate a distruggere quando sono diventate evidenti tutta la complessità di caratteristiche come position:fixed. La gerarchia dei livelli ha incrementato il numero di puntatori non locali aggiuntivi che indicano il livello superiore di scorrimento o "elemento principale clip" di uno strato e, in breve tempo, è stato molto difficile comprendere il codice.

Le strutture delle proprietà hanno risolto questo problema rappresentando l'effetto di scorrimento dell'overflow e dei clip dei contenuti separatamente da tutti gli altri effetti visivi. In questo modo è stato possibile modellare correttamente la vera struttura visiva e di scorrimento dei siti web. Poi, "Tutti" dovevamo implementare degli algoritmi sopra gli alberi delle proprietà, come la trasformazione dello schermo-spazio degli strati compositi o determinare quali strati scorrevano e quali no.

Infatti, abbiamo presto notato che c'erano molti altri punti nel codice in cui venivano sollevate domande geometriche simili. Il post sulle strutture di dati chiave offre un elenco più completo. Molti di essi presentavano implementazioni duplicate della stessa cosa faceva il codice del compositore; tutti avevano un sottoinsieme diverso di bug; e nessuna di queste ha modellato correttamente la vera struttura del sito web. La soluzione è diventata chiara: centralizzare tutti gli algoritmi di geometria in un unico posto e refactoring di tutto il codice per utilizzarlo.

Questi algoritmi a loro volta dipendono tutti dagli alberi delle proprietà, motivo per cui gli alberi di proprietà sono una struttura di dati chiave, ovvero utilizzata in tutta la pipeline, di RenderingNG. Per raggiungere questo obiettivo di codice geometrico centralizzato, abbiamo dovuto introdurre il concetto di strutture ad albero di proprietà molto prima nella pipeline, nel pre-paint, e modificare tutte le API che ora dipendevano da loro in modo che richiedessero l'esecuzione del pre-paint prima di poter essere eseguite.

Questo è un altro aspetto del pattern di refactoring di BlinkNG: identificare i calcoli chiave, eseguire il refactoring per evitare di duplicarli e creare fasi della pipeline ben definite che creano le strutture di dati che alimentano questi ultimi. Calcoliamo gli alberi delle proprietà esattamente nel momento in cui sono disponibili tutte le informazioni necessarie; e ci assicuriamo che le strutture di proprietà non possano cambiare durante l'esecuzione delle fasi di rendering successive.

Composito dopo la verniciatura: pipeline di vernice e compositing

La suddivisione è il processo per individuare quali contenuti DOM devono essere inseriti nel proprio livello composito (che, a sua volta, rappresenta una texture GPU). Prima del renderingNG, la stratificazione è stata eseguita prima della colorazione, non dopo (vedi qui la pipeline attuale; nota il cambio di ordine). Prima dovevamo decidere quali parti del DOM andavano in quale strato composito, poi disegnavamo degli elenchi di visualizzazione per queste texture. Ovviamente, le decisioni dipendevano da fattori quali elementi del DOM animati o scorrevoli oppure trasformazioni 3D e su quali elementi dipinti sopra.

Questo causava problemi gravi, perché richiedeva più o meno dipendenze circolari nel codice, un grosso problema per una pipeline di rendering. Vediamo perché con un esempio. Supponiamo di dover annullare la convalida del colore (nel senso che dobbiamo ridisegnare l'elenco di visualizzazione e poi rasterizzarlo di nuovo). La necessità di invalidare può derivare da una modifica nel DOM o da uno stile o layout modificato. Naturalmente, tuttavia, vorremmo invalidare solo le parti che sono state effettivamente cambiate. Ciò significava scoprire quali livelli compositi erano interessati, quindi invalidare parte o tutti gli elenchi di visualizzazione per questi livelli.

Ciò significa che l'annullamento della convalida dipendeva da DOM, stile, layout e decisioni di stratificazione precedenti (passato: significato per il frame sottoposto a rendering precedente). Ma l'attuale stratificazione dipende anche da tutti questi aspetti. E poiché non avevamo due copie di tutti i dati di stratificazione, era difficile distinguere tra le decisioni di stratificazione passate e future. Alla fine abbiamo quindi creato molto codice con un ragionamento circolare. Ciò portava a volte a codice illogico o errato oppure persino ad arresti anomali o problemi di sicurezza, se non siamo stati molto attenti.

Per far fronte a questa situazione, abbiamo introdotto inizialmente il concetto di oggetto DisableCompositingQueryAsserts. La maggior parte delle volte, se il codice tentava di eseguire query su decisioni di livello precedenti, causava un errore di asserzione e l'arresto anomalo del browser se era in modalità di debug. Questo ci ha aiutato a evitare l'introduzione di nuovi bug. In tutti i casi in cui il codice doveva legittimamente eseguire query sulle decisioni di stratificazione del passato, abbiamo inserito il codice per consentirlo allocando un oggetto DisableCompositingQueryAsserts.

Il nostro piano prevedeva, nel tempo, di eliminare tutti gli oggetti DisableCompositingQueryAssert dei siti di chiamata e poi dichiarare il codice sicuro e corretto. Tuttavia, abbiamo scoperto che una serie di chiamate è essenzialmente impossibile da rimuovere, purché la stratificazione sia stata eseguita prima della colorazione. Finalmente siamo riusciti a rimuoverlo solo molto di recente. Questo è stato il primo motivo scoperto per il progetto Composite After Paint. Abbiamo imparato che, anche se hai una fase della pipeline ben definita per un'operazione, se non si trova nella posizione sbagliata nella pipeline, alla fine rimarrai bloccato.

Il secondo motivo per il progetto Composite After Paint è stato dovuto al bug Fundamental Compositing. Un modo per sottolineare questo bug è che gli elementi DOM non sono una buona rappresentazione 1:1 di uno schema di stratificazione efficiente o completo per i contenuti di una pagina web. E poiché la composizione avveniva prima della colorazione, dipendeva più o meno intrinsecamente dagli elementi DOM, non dagli elenchi di visualizzazione o dalle strutture di proprietà. Questo è molto simile al motivo per cui abbiamo introdotto gli alberi delle proprietà e, proprio come per gli alberi delle proprietà, la soluzione c'è una soluzione diretta se si individua la fase giusta della pipeline, la esegui al momento giusto e le fornisci le strutture di dati chiave corrette. Come per gli alberi di proprietà, questa è stata una buona opportunità per garantire che, una volta completata la fase di colorazione, l'output sia immutabile per tutte le fasi successive della pipeline.

Vantaggi

Come hai visto, una pipeline di rendering ben definita produce enormi vantaggi a lungo termine. Esistono anche più di quanto pensi:

  • Affidabilità notevolmente migliorata: questa opzione è piuttosto semplice. Un codice più chiaro con interfacce ben definite e comprensibili è più facile da capire, scrivere e testare. Ciò lo rende più affidabile. Inoltre, rende il codice più sicuro e stabile, con meno arresti anomali e meno bug senza interruzioni.
  • Copertura dei test estesa: nel corso di BlinkNG, abbiamo aggiunto moltissimi nuovi test alla nostra suite. Sono inclusi i test delle unità che forniscono una verifica mirata degli interni; test di regressione che ci impediscono di reintrodurre i vecchi bug che abbiamo corretto (tantissimi!); e molte nuove aggiunte alla suite di test per le piattaforme web pubblica, gestita collettivamente, che tutti i browser utilizzano per misurare la conformità agli standard web.
  • Più facile da estendere: se un sistema è suddiviso in componenti chiari, non è necessario comprendere gli altri componenti a nessun livello di dettaglio per progredire rispetto a quello attuale. In questo modo è più facile per tutti aggiungere valore al codice di rendering senza dover essere un esperto, oltre a rendere più facile ragionare sul comportamento dell'intero sistema.
  • Prestazioni: l'ottimizzazione degli algoritmi scritti in codice "spaghetti" è abbastanza difficile, ma è quasi impossibile ottenere risultati ancora più grandi, come lo scorrimento con thread universale e le animazioni o i processi e 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.
  • Resa e contenimento: BlinkNG ha introdotto 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 sottoalberi che al momento non risultano pertinenti per l'utente? Questo è ciò che viene attivata la proprietà CSS content-visibility. Hai bisogno di fare in modo che lo stile di un componente dipenda dal layout? Ovvero le query container.

Case study: query sui container

Le query container sono una funzionalità molto attesa della piattaforma web che sta per iniziare ed è da anni la funzionalità numero uno più richiesto dagli sviluppatori CSS. Se è così grande, perché non esiste ancora? Il motivo è che l'implementazione di query container richiede una comprensione e un controllo molto meticolosi della relazione tra il codice di stile e il codice del layout. Diamo un'occhiata più da vicino.

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, è necessario eseguire il ricalcolo dello stile dopo il layout; ma il ricalcolo dello stile viene eseguito prima del layout. Questo paradosso delle uova è l'intero motivo per cui non siamo riusciti a implementare le query sui container prima di BlinkNG.

Come possiamo risolvere il problema? Non si tratta di una dipendenza retroattiva della pipeline, ovvero dello stesso problema risolto con progetti come Composite After Paint? Peggio ancora, cosa succede se i nuovi stili cambiano le dimensioni del predecessore? A volte questo non porta a un ciclo infinito?

In linea di principio, la dipendenza circolare può essere risolta utilizzando la proprietà CSS contenitori, che consente al rendering all'esterno di un elemento di non dipendere dal rendering all'interno del sottoalbero dell'elemento. Ciò significa che i nuovi stili applicati da un contenitore non possono influire sulle dimensioni del contenitore, perché le query del contenitore richiedono il contenimento.

Ma in realtà, ciò non era sufficiente, ed è stato necessario introdurre un tipo di contenimento più debole del semplice contenimento delle dimensioni. Questo perché è normale che un container di query container sia in grado di ridimensionarsi in una sola direzione (di solito a blocchi) in base alle dimensioni incorporate. Pertanto, è stato aggiunto il concetto di contenimento di dimensioni incorporate. Ma, come puoi vedere dalla nota molto lunga di questa sezione, per molto tempo non è stato chiaro se fosse possibile il contenimento delle dimensioni in linea.

Descrivere il contenimento nel linguaggio delle specifiche astratto è una cosa, ma un'altra cosa è implementarlo correttamente. Ricorda che uno degli obiettivi di BlinkNG era portare il principio di contenimento nelle "camminate" tra gli alberi, che costituisce la logica principale del rendering: quando si attraversa un sottoalbero, non dovrebbe essere richiesta alcuna informazione dall'esterno del sottoalbero. In un dato momento (beh, non è stato proprio un incidente), è molto più semplice e più pulito implementare il contenimento CSS se il codice di rendering rispetta il principio di contenimento.

Futuro: composizione fuori dal thread principale... e non solo!

La pipeline di rendering mostrata qui è in realtà un po' più avanti rispetto all'attuale implementazione di RenderingNG. Mostra la stratificazione come fuori dal thread principale, mentre al momento è ancora nel thread principale. Tuttavia, è solo questione di tempo prima che ciò avvenga, ora che Composite After Paint è stato spedito e la stratificazione è dopo la verniciatura.

Per comprendere perché questo aspetto è importante e dove possa condurre altri aspetti, dobbiamo considerare l'architettura del motore di rendering da una prospettiva un po' più elevata. Uno degli ostacoli più duraturi al miglioramento delle prestazioni di Chromium è il semplice fatto che il thread principale del renderer gestisca sia la logica principale dell'applicazione (ovvero l'esecuzione dello script) sia la maggior parte del rendering. Di conseguenza, il thread principale è spesso saturo 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 tempi di KHTML, quando l'esecuzione a thread singolo era il modello di programmazione dominante. Quando i processori multi-core sono diventati comuni nei dispositivi di livello consumer, il presupposto a thread unico era integrato a fondo in Blink (precedentemente WebKit). Volevamo introdurre più thread nel motore di rendering da molto tempo, ma nel vecchio sistema era semplicemente impossibile. Uno dei principali obiettivi del rendering NG era quello di liberarsi da questo vuoto e rendere possibile spostare il lavoro di rendering, in parte o per intero, su un altro thread (o thread).

Ora che BlinkNG sta per essere completato, stiamo già iniziando a esplorare quest'area; Il comit di non blocco è una prima incursione nella modifica del modello di thread del renderer. Il commit composito (o semplicemente commit) è un passaggio di sincronizzazione tra il thread principale e il thread del compositore. Durante il commit, creiamo copie dei dati di rendering prodotte sul thread principale, che verranno utilizzate dal codice di composizione downstream in esecuzione sul thread del compositore. Durante questa sincronizzazione, l'esecuzione del thread principale viene interrotta mentre il codice di copia è in esecuzione sul thread del compositore. Questo avviene per garantire che il thread principale non modifichi i dati di rendering mentre il thread del compositore li sta copiando.

Il commit senza blocco 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 di blocco sarà una riduzione del tempo dedicato al lavoro di rendering sul thread principale, che diminuirà la congestione sul thread principale e migliorerà le prestazioni. Al momento della stesura del presente documento (marzo 2022), disponiamo di un prototipo funzionante di Non-block Commit e ci stiamo preparando a effettuare un'analisi dettagliata del suo impatto sul rendimento.

In attesa nelle ali c'è la composizione del thread principale, con l'obiettivo di far corrispondere il motore di rendering all'illustrazione spostando la stratificazione dal thread principale e su un thread worker. Come per il commit non di blocco, questo ridurrà la congestione sul thread principale, diminuendo il carico di lavoro di rendering. Un progetto come questo non sarebbe stato possibile senza i miglioramenti architettonici di Composite After Paint.

E ci sono altri progetti nella pipeline. Abbiamo finalmente una base che permette di sperimentare la ridistribuzione del rendering e non vediamo l'ora di scoprire cosa sia possibile.