Analyse approfondie de RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Je m'appelle Ian Kilpatrick. ingénieur en chef dans l'équipe de mise en page de Blink, aux côtés de Koji Ishii. Avant de travailler dans l'équipe Blink, J'étais ingénieur front-end (avant que Google n'ait le rôle d'"ingénieur front-end"), créer des fonctionnalités dans Google Docs, Drive et Gmail. Après environ cinq ans à ce poste, j'ai pris le pari de rejoindre l'équipe de Blink, apprendre efficacement C au travail, et d'accroître le codebase Blink extrêmement complexe. Encore aujourd'hui, je ne comprends qu'une partie relativement petite. Je suis reconnaissant pour le temps qui m'a été accordé pendant cette période. J'ai été rassuré par le fait que de nombreuses tâches de "récupération d'ingénieurs front-end" sont passés au statut d'"ingénieur de navigateur" avant moi.

Mon expérience antérieure m'a aidé personnellement au sein de l'équipe Blink. En tant qu'ingénieur front-end, je rencontrais constamment des incohérences dans les navigateurs, les problèmes de performances, les bugs de rendu et les fonctionnalités manquantes. LayoutNG m'a permis d'aider à résoudre systématiquement ces problèmes dans le système de mise en page de Blink, et représente la somme des impressions de nombreux ingénieurs au fil des ans.

Dans ce post, je vais expliquer comment un tel changement d'architecture peut réduire et atténuer différents types de bugs et de problèmes de performances.

Vue à 9 000 mètres d'architectures de moteur de mise en page

Auparavant, l'arborescence de mise en page de Blink était ce que j'appellerai "arborescence modifiable".

Affiche l'arborescence comme décrit dans le texte suivant.

Chaque objet de l'arborescence de mise en page contenait des informations de type input. comme la taille disponible imposée par un parent, la position des nombres à virgule flottante et les informations de sortie (par exemple, la largeur et la hauteur finales de l'objet, ou ses positions X et Y).

Ces objets étaient conservés entre les rendus. En cas de changement de style, nous avons marqué cet objet comme sale et il en va de même pour tous ses parents dans l'arbre. Lorsque la phase de mise en page du pipeline de rendu a été exécutée, Ensuite, nous nettoyons l'arborescence, promenons tous les objets sales, puis nous exécutons la mise en page pour obtenir un état propre.

Nous avons constaté que cette architecture entraînait de nombreuses catégories de problèmes, que nous décrirons ci-dessous. Mais d'abord, prenons du recul et examinons les entrées et les sorties de la mise en page.

D'un point de vue conceptuel, l'exécution de la mise en page sur un nœud utilise le modèle "Style plus DOM", et toutes les contraintes parentes du système de mise en page parent (grille, bloc ou flexible), exécute l'algorithme de contrainte de mise en page et produit un résultat.

Le modèle conceptuel décrit précédemment.

Notre nouvelle architecture formalise ce modèle conceptuel. Nous disposons toujours de l'arborescence de mise en page, mais nous l'utilisons principalement pour conserver les entrées et les sorties de la mise en page. Pour le résultat, nous générons un tout nouvel objet immutable appelé immutable.

Arborescence des fragments.

J'ai abordé les une arborescence à fragments immuables, décrivant comment il est conçu pour réutiliser de grandes parties de l'arborescence précédente pour des mises en page incrémentielles.

De plus, nous stockons l'objet de contrainte parent qui a généré ce fragment. Nous l'utilisons comme clé de cache. Nous en reparlerons plus loin dans cet article.

L'algorithme de mise en page intégrée (texte) est également réécrit pour correspondre à la nouvelle architecture immuable. Non seulement elle génère représentation de liste plate immuable pour la mise en page intégrée, mais il offre également une mise en cache au niveau du paragraphe pour accélérer la remise en page. forme par paragraphe pour appliquer des caractéristiques de police aux éléments et aux mots, un nouvel algorithme bidirectionnel Unicode utilisant la bibliothèque ICU, de nombreuses corrections d'exactitude, et plus encore.

Types de bugs de mise en page

De manière générale, les bugs de mise en page relèvent de quatre catégories différentes : ayant chacune des causes différentes.

Exactitude

Lorsqu'on parle de bogues dans le système de rendu, on pense généralement à l'exactitude, Exemple : "Le comportement du navigateur A est de X, tandis que celui du navigateur B le comportement Y", ou "Les deux navigateurs A et B sont défaillants". Auparavant, c'est ce à quoi nous avons passé beaucoup de temps, et, au cours du processus, nous étions constamment aux prises avec le système. Un mode d'échec courant consistait à appliquer un correctif très ciblé pour un bug, mais nous découvrons des semaines plus tard que nous avions causé une régression dans une autre partie (apparemment non liée) du système.

Comme indiqué dans les posts précédents, c'est le signe d'un système très fragile. Pour la mise en page en particulier, nous n'avions pas de contrat propre entre les classes, ce qui oblige les ingénieurs du navigateur à dépendre d'un état, ce qu'ils ne devraient pas, ou mal interprété une valeur provenant d’une autre partie du système.

Par exemple, à un moment donné, nous avons eu une chaîne d'environ 10 bugs en plus d'un an, en lien avec la mise en page Flex. Chaque correction provoquait un problème d'exactitude ou de performance dans une partie du système, ce qui a entraîné un autre bug.

Maintenant que LayoutNG définit clairement le contrat entre tous les composants du système de mise en page, nous avons constaté que nous pouvons appliquer des changements avec beaucoup plus de confiance. Notre excellent projet Web Platform Tests (WPT) nous est également très utile, ce qui permet à plusieurs parties de contribuer à une suite de tests Web commune.

Aujourd'hui, nous constatons que si nous laissons une réelle régression sur notre version stable, il n'a généralement pas de tests associés dans le référentiel WPT, et ne résulte pas d'une incompréhension des contrats d'éléments individuels. De plus, dans le cadre de notre politique de correction de bugs, nous ajoutons toujours un nouveau test WPT, afin qu'aucun navigateur ne commet à nouveau la même erreur.

Sous-invalidation

Si vous avez déjà rencontré un bug mystérieux : le redimensionnement de la fenêtre du navigateur ou l'activation/désactivation d'une propriété CSS le font disparaître comme par magie, vous avez rencontré un problème de sous-invalidation. En effet, une partie de l'arbre modifiable a été considérée comme propre, mais en raison d'une modification des contraintes parentes, il ne représentait pas la sortie correcte.

C'est très courant pour les tests en (parcourant l'arborescence de mise en page deux fois pour déterminer l'état final de la mise en page) des modes de mise en page décrits ci-dessous. Auparavant, notre code se présentait comme suit:

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

Pour résoudre ce type de bug, procédez comme suit:

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

Une solution à ce type de problème entraînerait généralement une grave régression des performances, (voir l'invalidation excessive ci-dessous) et il était très délicat d'obtenir la bonne réponse.

Aujourd'hui (comme décrit ci-dessus), nous disposons d'un objet de contrainte parent immuable qui décrit toutes les entrées de la mise en page parent vers l'enfant. Nous le stockons avec le fragment immuable qui en résulte. C'est pourquoi nous disposons d'un emplacement centralisé où nous différencions ces deux entrées pour déterminer si l'élément enfant doit effectuer une autre passe de mise en page. Cette logique de différence est compliquée, mais bien contenue. Le débogage de cette catégorie de problèmes de sous-invalidation entraîne généralement une inspection manuelle des deux entrées. et déterminer ce qui a changé dans l'entrée de sorte qu'une autre passe de mise en page est requise.

Les corrections de ce code différent sont généralement simples, et facilement testables unitaires en raison de la simplicité de création de ces objets indépendants.

Comparer une image de largeur fixe à une image de largeur en pourcentage.
Un élément largeur/hauteur fixe n'a pas d'importance si la taille disponible qui lui est attribuée augmente, contrairement à une largeur/hauteur basée sur un pourcentage. La available-size est représentée dans l'objet available-size et, dans le cadre de l'algorithme de vérification différentielle (diffing), effectuera cette optimisation.

Le code différentiel pour l'exemple ci-dessus est le suivant:

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

Hystérèse

Cette catégorie de bugs est semblable à la sous-invalidation. Dans le système précédent, il était incroyablement difficile de s'assurer que la mise en page était idempotente, c'est-à-dire une mise en page avec les mêmes entrées, produit le même résultat.

Dans l'exemple ci-dessous, nous modifions simplement une propriété CSS entre deux valeurs. Il en résulte une "croissance infinie" rectangle.

La vidéo et la démonstration montrent un bug d'hystérèse dans Chrome 92 et les versions antérieures. Ce problème est corrigé dans Chrome 93.

Avec notre précédente arborescence modifiable, c'était incroyablement facile d'introduire des bugs comme celui-ci. Si le code a commis l'erreur de lire la taille ou la position d'un objet à un moment ou à une étape incorrects (car nous n'avons pas "effacé" la taille ou la position précédente, par exemple). nous ajoutons immédiatement un bug d'hystérèse subtil. Ces bugs n'apparaissent généralement pas lors des tests, car la majorité des tests portent sur une seule mise en page et un seul rendu. Plus inquiétant encore, nous savions qu'une partie de cette hystérèse était nécessaire pour que certains modes de mise en page fonctionnent correctement. Nous avions des bugs où nous procédions à une optimisation pour supprimer une passe de mise en page, mais introduisez un "bug" car le mode de mise en page nécessitait deux passes pour obtenir la sortie correcte.

Arborescence illustrant les problèmes décrits dans le texte précédent.
En fonction des informations de résultat de mise en page précédentes, les mises en page ne sont pas idempotentes

Avec LayoutNG, comme nous avons des structures de données d'entrée et de sortie explicites, et que l'accès à l'état précédent n'est pas autorisé, nous avons globalement réduit cette catégorie de bug dans le système de mise en page.

Invalidation excessive et performances

C'est le contraire de la classe de sous-invalidation des bugs. Souvent, lorsque nous corrigeons un bug de sous-invalidation, nous provoquons une baisse des performances.

Nous avons souvent dû faire des choix difficiles en faveur de l'exactitude plutôt que des performances. Dans la section suivante, nous expliquerons plus en détail comment nous avons atténué ces types de problèmes de performances.

Élévation des configurations en deux passes et performances des falaises

La mise en page flexible et en grille représentait un changement dans l'expressivité des mises en page sur le Web. Cependant, ces algorithmes étaient fondamentalement différents de l'algorithme de mise en page en blocs qui les a précédés.

Dans presque tous les cas, la mise en page en bloc ne nécessite que le moteur effectue la mise en page sur tous ses enfants une seule fois. Cela permet d'améliorer les performances, mais finit par ne pas être aussi expressif que le souhaitent les développeurs Web.

Par exemple, souvent, vous voulez que la taille de tous les enfants soit étendue à la taille du plus grand. Pour ce faire, la mise en page parent (flex ou grille) exécute une mesure pour déterminer la taille de chacun des enfants, puis une passe de mise en page pour étirer tous les enfants à cette taille. Ce comportement est le comportement par défaut pour les mises en page modulables et les mises en page en grille.

Deux ensembles de boîtes, le premier indique la taille intrinsèque des boîtes dans le pass de mesure, le second avec une mise en page de hauteur égale.

Ces mises en page en deux passages étaient initialement acceptables en termes de performances, car les gens ne les imbriquaient généralement pas profondément. Toutefois, nous avons commencé à constater d'importants problèmes de performances à mesure que des contenus plus complexes étaient apparus. Si vous ne mettez pas en cache le résultat de la phase de mesure, l'arborescence de mise en page plante entre son état de mesure et son état final de mise en page.

Les mises en page en un, deux et trois passages expliquées dans la légende.
Dans l'image ci-dessus, nous avons trois éléments <div>. Une mise en page simple en un passage (comme une disposition en blocs) visite trois nœuds de mise en page (complexité O(n)). En revanche, pour une mise en page en deux passes (flex ou grille), cela peut potentiellement compliquer les visites O(2n) pour cet exemple.
Graphique illustrant l&#39;augmentation exponentielle du temps de mise en page
Cette image et cette démonstration montrent une mise en page exponentielle en mode Grille. Ce problème est résolu dans Chrome 93 suite au déplacement de la grille vers la nouvelle architecture.

Auparavant, nous avions essayé d'ajouter des caches très spécifiques aux mises en page flexible et en grille afin de lutter contre ce type de baisse des performances. Cela a marché (et nous sommes allés très loin avec Flex), mais qui se battaient constamment avec des bugs d'invalidation sous- et excessifs.

LayoutNG permet de créer des structures de données explicites pour l'entrée et la sortie de la mise en page. Nous avons également créé des caches pour les passes de mesure et de mise en page. Cela ramène la complexité à O(n), ce qui se traduit par des performances linéaires et prévisibles pour les développeurs Web. Si une mise en page utilise une mise en page en trois passes, nous mettrons simplement en cache ces passes également. Cela peut permettre d'introduire en toute sécurité des modes de mise en page plus avancés à l'avenir. Un exemple de la façon dont RenderingNG, fondamentalement d'extensibilité à tous les niveaux. Dans certains cas, la mise en page en grille peut nécessiter des mises en page en trois passages, mais c'est extrêmement rare pour le moment.

Nous constatons que lorsque les développeurs rencontrent des problèmes de performances en particulier avec la mise en page, cela est généralement dû à un bug de temps de mise en page exponentiel plutôt qu'au débit brut de l'étape de mise en page du pipeline. Si une petite modification incrémentielle (un élément modifiant une seule propriété CSS) entraîne une mise en page de 50 à 100 ms, il s'agit probablement d'un bug de mise en page exponentiel.

En résumé

La mise en page est un domaine extrêmement complexe, Nous n'avons pas abordé toutes sortes de détails intéressants, comme les optimisations intégrées (comment fonctionne le sous-système de texte et de texte), Les concepts évoqués ici n'ont fait qu'effleurer la surface. et a ignoré de nombreux détails. Cependant, nous espérons avoir montré comment l'amélioration systématique de l'architecture d'un système peut entraîner des gains considérables à long terme.

Cela dit, nous savons que nous avons encore beaucoup de travail devant nous. Nous sommes conscients des différents types de problèmes (de performances et d'exactitude) que nous cherchons à résoudre. et nous avons hâte de voir les nouvelles fonctionnalités de mise en page bientôt disponibles. Nous pensons que l'architecture de LayoutNG rend la résolution de ces problèmes facile et sécurisée.

Une image (vous savez laquelle !) d'Una Kravets.