Approfondimento su RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Sono Ian Kilpatrick, ingegnere del team di layout di Blink, insieme a Koji Ishii. Prima di lavorare nel team di Blink, Ero un front-end engineer (prima che Google assumesse il ruolo di "front-end engineer"), per creare funzionalità di Documenti Google, Drive e Gmail. Dopo circa cinque anni in quel ruolo ho preso una grande scommessa passando al team dei Blink, per apprendere efficacemente il C++ sul lavoro, e il tentativo di ampliare il codebase Blink, estremamente complesso. Ancora oggi ne conosco solo una parte relativamente limitata. Sono grata per il tempo che mi è stato concesso in questo periodo. Mi ha confortato il fatto che molti ingegneri di front-end "recuperano" è passata a essere un "browser engineer" davanti a me.

La mia esperienza precedente mi ha guidata personalmente nel team di Blink. In qualità di front-end engineer, ho dovuto affrontare costantemente incoerenze nel browser, problemi di prestazioni, bug di rendering e funzionalità mancanti. LayoutNG mi è stato utile per risolvere sistematicamente questi problemi nel sistema di layout di Blink. e rappresenta la somma dei dati di molti ingegneri impegno nel corso degli anni.

In questo post, spiegherò come una grande modifica all'architettura come questa può ridurre e mitigare vari tipi di bug e problemi di prestazioni.

Vista di 9000 metri di architetture dei motori di layout

In precedenza, l'albero di layout di Blink era quello che chiamerò "albero modificabile".

Mostra l'albero come descritto nel testo che segue.

Ogni oggetto nella struttura ad albero del layout conteneva informazioni di input, come la dimensione disponibile imposta da un publisher principale, la posizione di qualsiasi numero in virgola mobile e le informazioni di output, ad esempio la larghezza e l'altezza finali dell'oggetto o le relative posizioni x e y.

Questi oggetti sono stati conservati tra un rendering e l'altro. Quando si verificava un cambiamento di stile, abbiamo contrassegnato quell'oggetto come sporco e allo stesso modo tutti i suoi genitori nell'albero. Quando è stata eseguita la fase di layout della pipeline di rendering, Puliamo l'albero, percorriamo eventuali oggetti sporchi e quindi eseguiamo il layout per riportarli a uno stato pulito.

Abbiamo scoperto che questa architettura causava molte classi di problemi. come descritto di seguito. Ma prima facciamo un passo indietro e consideriamo quali sono gli input e gli output del layout.

Il layout in esecuzione su un nodo in questo albero utilizza concettualmente lo "Stile più DOM", ed eventuali vincoli padre del sistema di layout padre (griglia, blocco o flessibile), esegue l'algoritmo del vincolo del layout e produce un risultato.

Il modello concettuale descritto in precedenza.

La nostra nuova architettura formalizza questo modello concettuale. Abbiamo ancora la struttura ad albero del layout, ma la usiamo principalmente per memorizzare gli input e gli output del layout. Per l'output, generiamo un oggetto completamente nuovo e immutabile chiamato albero dei frammenti.

L'albero dei frammenti.

Ho esaminato albero di frammenti immutabile in precedenza, che descrive come è progettato per riutilizzare grandi parti dell'albero precedente per layout incrementali.

Inoltre, memorizziamo l'oggetto vincoli padre che ha generato il frammento. Viene utilizzata come chiave cache, di cui parleremo più avanti.

Anche l'algoritmo di layout in linea (testo) viene riscritto per adeguarsi alla nuova architettura immutabile. Non produce solo rappresentazione flat list immutabile per il layout in linea, ma offre anche la memorizzazione nella cache a livello di paragrafo per un relayout più rapido. shape-per-paragraph per applicare caratteristiche di carattere a elementi e parole, un nuovo algoritmo bidirezionale Unicode che utilizza la terapia intensiva, molte correzioni di correttezza e altro ancora.

Tipi di bug relativi al layout

I bug di layout rientrano, in generale, in quattro diverse categorie: ognuna con cause principali diverse.

Correttezza

Quando parliamo di bug nel sistema di rendering, di solito prendiamo in considerazione la correttezza, ad esempio: "Il browser A ha un comportamento X, mentre il browser B ha un comportamento Y", o "I browser A e B non funzionano". In precedenza abbiamo dedicato gran parte del nostro tempo a questo e al contempo litigavamo costantemente con il sistema. Una modalità di errore comune consisteva nell'applicazione di una correzione molto mirata a un bug, ma scopriamo settimane dopo che avevamo causato una regressione in un'altra parte (apparentemente non correlata) del sistema.

Come descritto nei post precedenti, questo è il segnale di un sistema molto fragile. Per quanto riguarda il layout in particolare, non c'era un contratto chiaro tra nessuna classe, facendo sì che i tecnici del browser dipendano dallo stato che non dovrebbero, o interpretare erroneamente un valore di un'altra parte del sistema.

Ad esempio, a un certo punto avevamo una catena di circa 10 insetti in più di un anno, relative al layout flessibile. Ogni correzione ha causato un problema di correttezza o prestazioni in una parte del sistema, che ha causato un altro bug.

Ora che LayoutNG definisce chiaramente il contratto tra tutti i componenti nel sistema di layout, abbiamo visto che possiamo applicare le modifiche con maggiore sicurezza. Inoltre, tragiamo vantaggio dall'eccellente progetto Web Platform Tests (WPT), che consente a più parti di contribuire a una suite di test web comune.

Oggi scopriamo che se rilasciamo una regressione reale sul nostro canale stabile, in genere non ha test associati nel repository WPT, e non è frutto di un'incomprensione dei contratti che compongono i componenti. Inoltre, nell'ambito delle nostre norme relative alla correzione di bug, aggiungiamo sempre un nuovo test WPT, per fare in modo che nessun browser debba ripetere lo stesso errore.

Non valida

Se ti è mai capitato di riscontrare un bug misterioso per cui il ridimensionamento della finestra del browser o l'attivazione/disattivazione di una proprietà CSS lo eliminano magicamente, hai riscontrato un problema di sotto-invalidazione. In effetti, una parte dell'albero mutabile era considerata pulita, ma a causa di alcune modifiche nei vincoli padre non rappresentava l'output corretto.

Questo è molto comune con la configurazione (camminando due volte sull'albero del layout per determinare lo stato finale del layout) le modalità di layout descritte di seguito. In precedenza, il nostro codice avrebbe questo aspetto:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

In genere, una correzione di questo tipo di bug potrebbe essere:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

Una correzione di questo tipo di problema in genere causa una grave regressione del rendimento. (vedi l'invalidazione eccessiva di seguito) ed è stato molto delicato fare le corrette.

Oggi (come descritto sopra) abbiamo un oggetto vincoli padre immutabile che descrive tutti gli input dal layout principale a quello figlio. Lo memorizziamo insieme al frammento immutabile risultante. Per questo motivo, abbiamo una posizione centralizzata in cui confrontiamo questi due input per determinare se il publisher secondario deve eseguire un'altra verifica del layout. Questa logica differente è complicata, ma ben isolata. Il debug di questa classe di problemi di invalidità in genere comporta l'ispezione manuale dei due input e decidendo cosa è cambiato nell'input in modo che sia necessario un altro passaggio al layout.

Le correzioni a questo codice sono in genere semplici e facilmente testabili grazie alla semplicità di creazione di questi oggetti indipendenti.

Confronto di un'immagine con larghezza fissa e percentuale con larghezza.
. A un elemento di larghezza/altezza fissa non importa se la dimensione disponibile assegnata aumenta, mentre la larghezza/altezza in base alla percentuale aumenta. La dimensione available-size è rappresentata nell'oggetto Vincoli padre e, come parte dell'algoritmo di confronto, eseguirà questa ottimizzazione.

Il codice di differenziazione per l'esempio precedente è:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

Isteresi

Questa classe di bug è simile alla sotto-invalidazione. In pratica, nel sistema precedente era estremamente difficile garantire che il layout fosse idempotente, cioè rieseguire il layout con gli stessi input, ha restituito lo stesso output.

Nell'esempio riportato di seguito, stiamo semplicemente spostando una proprietà CSS avanti e indietro tra due valori. Tuttavia, questo si traduce in una "infinita crescita" rettangolo.

Il video e la demo mostrano un bug di isteresi in Chrome 92 e versioni precedenti. Il problema è stato risolto in Chrome 93.

Con il nostro precedente albero modificabile, è stato facilissimo introdurre bug come questo. Se il codice ha commesso l'errore di leggere la dimensione o la posizione di un oggetto in un momento o una fase errati (poiché non abbiamo "cancellato" ad esempio le dimensioni o la posizione precedenti), aggiungeremmo immediatamente un piccolo bug di isteresi. Questi bug in genere non compaiono nei test, poiché la maggior parte dei test si concentra su un singolo layout e su un singolo rendering. Ancora più preoccupante, sapevamo che una parte di questa isteresi era necessaria per far funzionare correttamente alcune modalità di layout. Avevamo dei bug in cui dovevamo eseguire un'ottimizzazione per rimuovere un pass per il layout, ma introduci un "bug" poiché la modalità di layout richiedeva due passaggi per ottenere l'output corretto.

Un albero che mostra i problemi descritti nel testo precedente.
. A seconda delle informazioni precedenti sui risultati di layout, produrrà layout non idempotenti

LayoutNG, poiché disponiamo di strutture di dati di input e output espliciti, e l'accesso allo stato precedente non è consentito, abbiamo mitigato ampiamente questa classe di bug dal sistema di layout.

Prestazioni e invalidazione eccessive

È l'esatto opposto della classe di bug di sottoinvalidazione. Spesso, la correzione di un bug di sottoinvalidazione attivava un limite di prestazioni.

Spesso abbiamo dovuto fare scelte difficili a favore della correttezza rispetto al rendimento. Nella prossima sezione analizzeremo più in dettaglio come abbiamo mitigato questi tipi di problemi di rendimento.

Aumento dei layout a due passaggi e scarpate di rendimento

Layout a griglia e flessibile hanno rappresentato un cambiamento nell'espressività dei layout sul web. Tuttavia, questi algoritmi erano fondamentalmente diversi dall'algoritmo di layout a blocchi precedente.

Il layout a blocchi (nella maggior parte dei casi) richiede che il motore esegua il layout su tutti i suoi elementi figlio esattamente una volta. È un ottimo risultato per migliorare il rendimento, ma risulta non essere così espressivo come vogliono gli sviluppatori web.

Ad esempio: spesso si desidera che la dimensione di tutti gli elementi secondari si espanda alla dimensione più grande. A supporto di ciò, il layout principale (flessibile o griglia) esegue un pass di misura per determinare le dimensioni di ciascuno poi una pass di layout per estendere tutti gli elementi secondari a queste dimensioni. Questo comportamento è quello predefinito sia per il layout flessibile sia per quello a griglia.

Due serie di scatole: la prima mostra le dimensioni intrinseche dei scatole nel passo di misurazione, la seconda in layout, tutte a stessa altezza.

Questi layout a due passaggi erano inizialmente accettabili dal punto di vista delle prestazioni, perché le persone solitamente non li nidificavano profondamente. Tuttavia, con l'emergere di contenuti più complessi, abbiamo iniziato a notare problemi di rendimento significativi. Se non memorizzi nella cache il risultato della fase di misurazione, lo stato measure della struttura ad albero di layout e lo stato layout finale.

I layout a 1, 2 e 3 passaggi sono spiegati nella didascalia.
. Nell'immagine qui sopra, sono presenti tre elementi <div>. Un semplice layout one-pass (come il layout a blocchi) visita tre nodi di layout (complessità O(n)). Tuttavia, per un layout a due passaggi (ad esempio flex o griglia), Ciò può potenzialmente comportare una complessità del numero di visite di O(2n) per questo esempio.
di Gemini Advanced.
.
Grafico che mostra l&#39;aumento esponenziale del tempo di layout.
. Questa immagine e questa demo mostrano un layout esponenziale con layout griglia. Questo problema è stato risolto in Chrome 93 a seguito del trasferimento della griglia alla nuova architettura
di Gemini Advanced.
.

In precedenza, cercavamo di aggiungere cache molto specifiche al layout flessibile e a griglia per contrastare questo tipo di problema di rendimento. Ha funzionato (e con Flex siamo andati molto lontano), ma lottavamo costantemente con i bug di invalidazione in eccesso.

LayoutNG ci consente di creare strutture di dati esplicite sia per l'input che per l'output del layout, Inoltre, abbiamo creato cache delle tessere di misura e di layout. Questo riporta la complessità a O(n), generando prestazioni prevedibilmente lineari per gli sviluppatori web. Nel caso in cui si verifichi un caso in cui un layout utilizzi un layout a tre passaggi, provvederemo semplicemente a memorizzare nella cache anche la tessera. In futuro, ciò potrebbe aprire l'opportunità di introdurre in modo sicuro modalità di layout più avanzate: un esempio di come RenderingNG fondamentalmente sblocca l'estensibilità su tutta la linea. In alcuni casi, il layout a griglia può richiedere layout a tre passaggi, ma al momento è estremamente raro.

Se gli sviluppatori riscontrano problemi di prestazioni in particolare con il layout, è tipicamente dovuto a un bug esponenziale del tempo di layout piuttosto che alla velocità effettiva non elaborata della fase di layout della pipeline. Se una piccola modifica incrementale (un elemento che modifica una singola proprietà CSS) fa sì che il layout sia compreso tra 50 e 100 ms, probabilmente si tratta di un bug di layout esponenziale.

In sintesi

Layout è un'area estremamente complessa, e non abbiamo trattato tutti i tipi di dettagli interessanti, come le ottimizzazioni del layout in linea (come funziona l'intero sottosistema in linea e di testo), e persino i concetti di cui abbiamo parlato qui hanno solo scalfito la superficie, e lucidato a molti dettagli. Tuttavia, ci auguriamo di aver dimostrato come il miglioramento sistematico dell'architettura di un sistema possa portare a enormi guadagni sul lungo periodo.

Detto questo, sappiamo che abbiamo ancora molto lavoro davanti a noi. Siamo consapevoli delle classi di problemi (prestazioni e correttezza) che stiamo cercando di risolvere. e siamo entusiasti delle nuove funzionalità di layout in arrivo in CSS. Crediamo che l'architettura di LayoutNG renda la risoluzione di questi problemi sicura e trattabile.

Un'immagine (sai quale!) di Una Kravets.