Rendu NG en profondeur: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Blink fait référence à l'implémentation par Chromium de la plate-forme Web. Il englobe toutes les phases d'affichage avant la composition, qui se terminent par la validation du compositeur. Pour en savoir plus sur l'architecture de rendu clignotant, consultez un article précédent de cette série.

Blink a vu le jour en tant que duplication de WebKit, qui est lui-même une copie de KHTML, qui remonte à 1998. Il contient l'un des codes les plus anciens (et les plus importants) de Chromium. En 2014, il montre clairement son âge. Cette année-là, nous nous sommes lancés dans une série de projets ambitieux sous le nom de BlinkNG, dans le but de combler des lacunes de longue date dans l'organisation et la structure du code Blink. Cet article explore BlinkNG et ses projets: pourquoi nous l'avons fait, ce qu'il a accompli, les principes directeurs qui ont façonné sa conception et les possibilités d'amélioration future qu'il offre.

Pipeline de rendu avant et après BlinkNG.

Rendu pré-NG

D'un point de vue conceptuel, le pipeline de rendu de Blink était toujours divisé en phases (style, layout, paint, etc.), mais les barrières d'abstraction n'étaient pas claires. De manière générale, les données associées à l'affichage sont constituées d'objets modifiables de longue durée. Ces objets pouvaient être modifiés à tout moment et étaient fréquemment recyclés et réutilisés lors de mises à jour successives de l'affichage. Il était impossible de répondre de façon fiable à des questions simples telles que:

  • La sortie du style, de la mise en page ou de la peinture doit-elle être mise à jour ?
  • Quand ces données obtiendront-elles leur valeur "finale" ?
  • Quand est-il possible de modifier ces données ?
  • Quand cet objet sera-t-il supprimé ?

Voici quelques exemples:

Le style générait des ComputedStyles en fonction des feuilles de style, mais ComputedStyle n'était pas immuable. Dans certains cas, il serait modifié par des étapes ultérieures du pipeline.

Style génère une arborescence de LayoutObject, puis layout annote ces objets avec des informations de taille et de positionnement. Dans certains cas, layout modifie même l'arborescence. Il n'y a pas de séparation claire entre les entrées et les sorties de layout.

L'élément Style génère des structures de données accessoires qui ont déterminé le déroulement de la composition. Ces structures de données étaient modifiées à chaque phase après le style.

À un niveau inférieur, les types de données de rendu se composent en grande partie d'arborescences spécialisées (par exemple, l'arborescence DOM, l'arborescence de styles, l'arborescence de mise en page et l'arborescence de propriétés Paint). Les phases de rendu sont implémentées sous la forme de parcours dans les arbres récursifs. Idéalement, un parcours dans l'arborescence doit être contenant: lors du traitement d'un nœud d'arbre donné, nous ne devons accéder à aucune information en dehors de la sous-arborescence racine sur ce nœud. Cela n'a jamais été vrai avant le rendu NG ; l'arbre parcourt des informations fréquemment consultées auprès des ancêtres du nœud en cours de traitement. Cela rendait le système très fragile et sujet aux erreurs. De plus, il était impossible de commencer à marcher dans un arbre depuis sa racine.

Enfin, de nombreuses rampes d'accès au pipeline de rendu sont éparpillées dans le code: mises en page forcées déclenchées par JavaScript, mises à jour partielles déclenchées lors du chargement du document, mises à jour forcées pour préparer le ciblage d'événements, mises à jour planifiées demandées par le système d'affichage et API spécialisées exposées uniquement pour tester le code, pour n'en citer que quelques-unes. Le pipeline de rendu comportait même quelques chemins récursifs et réentrants (c'est-à-dire passer au début d'une étape à partir du milieu d'une autre). Chacune de ces rampes possédait son propre comportement et, dans certains cas, la sortie de rendu dépendait de la manière dont la mise à jour de l'affichage a été déclenchée.

Ce que nous avons changé

BlinkNG est composé de nombreux sous-projets, petits ou grands, avec pour objectif commun d'éliminer les déficits architecturaux décrits précédemment. Ces projets partagent quelques principes directeurs conçus pour que le pipeline de rendu ressemble davantage à un véritable pipeline:

  • Point d'entrée uniforme: nous devons toujours saisir le pipeline au début.
  • Phases fonctionnelles: chaque étape doit comporter des entrées et des sorties bien définies, et son comportement doit être fonctionnel, c'est-à-dire déterministe et reproductible. Les sorties doivent également dépendre uniquement des entrées définies.
  • Entrées constantes: les entrées de toute étape doivent être constantes pendant que la phase est en cours d'exécution.
  • Sorties immuables: une fois qu'une étape est terminée, ses sorties doivent rester immuables pour le reste de la mise à jour du rendu.
  • Cohérence des points de contrôle: à la fin de chaque étape, les données de rendu produites jusqu'à présent doivent être dans un état autocohérent.
  • Déduplication du travail: ne calculez chaque élément qu'une seule fois.

Une liste complète des sous-projets BlinkNG serait fastidieuse à lire, mais en voici quelques-unes qui ont des conséquences particulières.

Cycle de vie des documents

La classe DocumentLifecycle suit notre progression dans le pipeline de rendu. Il nous permet d'effectuer des vérifications de base qui appliquent les invariantes énumérées précédemment, par exemple:

  • Si vous modifiez une propriété ComputedStyle, le cycle de vie du document doit être kInStyleRecalc.
  • Si l'état de DocumentLifecycle est kStyleClean ou ultérieur, NeedsStyleRecalc() doit renvoyer false pour tous les nœuds associés.
  • Lorsque vous accédez à la phase du cycle de vie paint, l'état du cycle de vie doit être kPrePaintClean.

Au cours de l'implémentation de BlinkNG, nous avons systématiquement éliminé les chemins de code qui ne respectaient pas ces règles invariantes et avons éparpillé beaucoup plus d'assertions dans le code pour éviter toute régression.

Si vous êtes déjà allé dans le terrier du lapin en regardant du code de rendu de bas niveau, vous vous demandez peut-être comment vous êtes arrivé jusqu'ici. Comme indiqué précédemment, il existe différents points d'entrée dans le pipeline de rendu. Auparavant, cela incluait les chemins d'appel récursifs et réentrants, ainsi que les endroits où nous entrons dans le pipeline lors d'une phase intermédiaire, plutôt qu'au début. Dans le cadre de BlinkNG, nous avons analysé ces chemins d'appel et avons constaté qu'ils étaient tous réduits à deux scénarios de base:

  • Toutes les données de rendu doivent être mises à jour, par exemple lors de la génération de nouveaux pixels pour le Réseau Display ou lors d'un test de positionnement pour le ciblage d'événements.
  • Nous avons besoin d'une valeur à jour pour une requête spécifique à laquelle nous pouvons répondre sans mettre à jour toutes les données de rendu. Cela inclut la plupart des requêtes JavaScript (par exemple, node.offsetTop).

Il n'existe désormais plus que deux points d'entrée dans le pipeline de rendu, correspondant à ces deux scénarios. Les chemins de code réentrants ont été supprimés ou refactorisés, et il n'est plus possible d'entrer dans le pipeline à partir d'une phase intermédiaire. Cela a éliminé de nombreux mystères concernant exactement quand et comment les mises à jour de l'affichage se produisent, ce qui permet de mieux comprendre le comportement du système.

Style, mise en page et pré-peinture

Collectivement, les phases d'affichage avant l'application paint sont responsables des éléments suivants:

  • Exécution de l'algorithme cascade de styles pour calculer les propriétés de style finales pour les nœuds DOM
  • Génération de l'arborescence de mise en page représentant la hiérarchie des zones du document
  • Déterminer les informations de taille et de position de tous les champs.
  • Arrondissement ou ancrage d'une géométrie sous un pixel pour respecter les limites de pixels entiers pour la peinture
  • Déterminer les propriétés des couches composées (transformation affine, filtres, opacité ou toute autre accélération GPU)
  • Déterminer le contenu qui a été modifié depuis la dernière phase de peinture et qui doit être peint ou repeint (invalidation de peinture)

Cette liste n'a pas changé, mais avant BlinkNG, l'essentiel de ce travail était effectué de manière ad hoc, répartie sur plusieurs phases de rendu, avec de nombreuses fonctionnalités dupliquées et des inefficacités intégrées. Par exemple, la phase de style a toujours été principalement responsable du calcul des propriétés de style finales pour les nœuds. Toutefois, dans certains cas particuliers, nous ne déterminions les valeurs des propriétés de style finales qu'une fois la phase de style terminée. Dans le processus d'affichage, il n'y a pas eu de point formel ni exécutoire permettant de dire avec certitude que les informations de style étaient complètes et immuables.

Un autre bon exemple de problème pré-BlinkNG est l'invalidation de peinture. Auparavant, l'invalidation de l'application était éparpillée tout au long de toutes les phases de rendu, jusqu'au rendu. Lors de la modification du style ou du code de mise en page, il était difficile de savoir quelles modifications étaient nécessaires pour peindre la logique d'invalidation. De plus, il était facile de commettre une erreur conduisant à des bugs d'invalidation trop ou pas assez élevés. Pour en savoir plus sur les subtilités de l'ancien système d'invalidation de peinture, consultez l'article de cette série consacré à LayoutNG.

L'ancrage d'une géométrie de mise en page des sous-pixels aux limites de pixels entiers pour la peinture est un exemple de cas où nous avions plusieurs implémentations de la même fonctionnalité et que nous avions fait beaucoup de tâches redondantes. Le système Paint utilisait un chemin de code d'ancrage de pixels, tandis qu'un autre chemin de code entièrement distinct était utilisé chaque fois que nous avions besoin d'un calcul ponctuel et ponctuel des coordonnées intégrées aux pixels en dehors du code peint. Il va sans dire que chaque implémentation présentait ses propres bugs et que les résultats obtenus ne correspondaient pas toujours. Comme ces informations n'étaient pas mises en cache, le système effectuait parfois exactement le même calcul à plusieurs reprises, ce qui représentait une pression supplémentaire sur les performances.

Voici quelques projets importants qui ont éliminé les déficits architecturaux des phases de rendu avant l'application de la peinture.

Project Squad: pipeline de la phase de style

Ce projet a fait face à deux lacunes principales lors de la phase de style, ce qui a empêché son bon déroulement:

La phase de style a deux sorties principales: ComputedStyle, qui contient le résultat de l'exécution de l'algorithme de cascade CSS sur l'arborescence DOM, et une arborescence LayoutObjects, qui établit l'ordre des opérations pour la phase de mise en page. Conceptuellement, l'exécution de l'algorithme Cascade devrait se faire uniquement avant de générer l'arborescence de mise en page. Mais auparavant, ces deux opérations étaient entrelacées. Project Squad a réussi à diviser ces deux éléments en phases distinctes et séquentielles.

Auparavant, ComputedStyle n'obtenait pas toujours sa valeur finale lors du recalcul du style. Dans certains cas, ComputedStyle était mis à jour lors d'une phase ultérieure du pipeline. Project Squad a correctement refactorisé ces chemins de code, de sorte que ComputedStyle ne soit jamais modifié après la phase de style.

LayoutNG: adaptation de la phase de mise en page

Ce projet monumental, l'un des piliers de RenderingNG, était une réécriture complète de la phase de rendu de la mise en page. Nous ne ferons pas honneur à l'ensemble du projet ici, mais il y a quelques aspects notables pour le projet BlinkNG dans son ensemble:

  • Précédemment, la phase de mise en page recevait une arborescence de LayoutObject créée par la phase de style, et y ajoutait des informations sur la taille et la position. Il n'y a donc pas de séparation claire entre les entrées et les sorties. LayoutNG a introduit l'arborescence de fragments, qui est la sortie principale en lecture seule de la mise en page et sert d'entrée principale pour les phases de rendu suivantes.
  • LayoutNG a introduit la propriété de conteneur dans la mise en page: lors du calcul de la taille et de la position d'une LayoutObject donnée, nous ne regardons plus en dehors de la sous-arborescence racine de cet objet. Toutes les informations nécessaires pour mettre à jour la mise en page d'un objet donné sont calculées à l'avance et fournies sous forme d'entrée en lecture seule à l'algorithme.
  • Auparavant, il y avait des cas particuliers où l'algorithme de mise en page n'était pas strictement fonctionnel: le résultat de l'algorithme dépendait de la dernière mise à jour de la mise en page. LayoutNG a éliminé ces cas de figure.

La phase de pré-peinture

Auparavant, il n'y avait pas de phase formelle de rendu pré-peinture, juste une phase d'opérations post-mise en page. La phase de pré-peinture est née du fait que certaines fonctions connexes pouvaient être implémentées de manière optimale en effectuant un balayage systématique de l'arborescence de mise en page une fois la mise en page terminée. Plus important encore:

  • Émettre des invalidations de peinture: il est très difficile d'effectuer correctement l'invalidation de peinture au cours de la mise en page, lorsque les informations sont incomplètes. Il est beaucoup plus facile et efficace d'y arriver, s'il est divisé en deux processus distincts: pour le style et la mise en page, le contenu peut être marqué à l'aide d'un simple indicateur booléen, car il est possible qu'il nécessite une invalidation de peinture. Lors de cette étape, nous vérifions ces signalements et émettons des invalidations si nécessaire.
  • Générer des arbres de propriété de peinture: processus décrit plus en détail par la suite.
  • Calcul et enregistrement des emplacements de peinture de pixels: les résultats enregistrés peuvent être utilisés par la phase de peinture, ainsi que par tout code en aval qui en a besoin, sans calcul redondant.

Arbres immobiliers: une géométrie cohérente

Les arbres de propriétés ont été introduits au début de RenderingNG pour faire face à la complexité du défilement, qui sur le Web a une structure différente de tous les autres types d'effets visuels. Avant les arborescences de propriétés, le compositeur de Chromium utilisait une hiérarchie de "calques " unique pour représenter la relation géométrique du contenu composé, mais celle-ci s'est rapidement détachée à mesure que la complexité complète des caractéristiques telles que position:fixe est devenue évidente. La hiérarchie des calques a augmenté le nombre de pointeurs non locaux supplémentaires, indiquant le "parent de défilement" ou le "parent de l'extrait" d'un calque. Très rapidement, il était très difficile de comprendre le code.

Les arborescences de propriétés ont résolu ce problème en représentant séparément les aspects de défilement et de rognage du contenu par rapport à tous les autres effets visuels. Cela a permis de modéliser correctement la véritable structure visuelle et de défilement des sites Web. Ensuite, nous devions implémenter des algorithmes au-dessus des arborescences de propriétés, comme la transformation de l'espace d'écran des couches composées, ou déterminer quelles couches faisaient défiler ou non.

En fait, nous avons rapidement remarqué que de nombreux autres endroits du code renvoyaient des questions géométriques similaires. (Vous trouverez une liste plus complète dans le post sur les principales structures de données.) Plusieurs d'entre eux avaient des implémentations en double de la même chose que le code du compositeur ; toutes comportaient un sous-ensemble différent de bugs ; et aucun d'entre eux ne modélisait correctement la véritable structure du site Web. La solution s'est alors imposée comme une évidence: centraliser tous les algorithmes de géométrie au même endroit et refactoriser tout le code pour les utiliser.

Ces algorithmes dépendent tous des arborescences de propriétés. C'est pourquoi les arborescences de propriétés constituent une structure de données clé, c'est-à-dire une structure de données utilisée tout au long du pipeline de RenderingNG. Pour atteindre cet objectif de code géométrique centralisé, nous avons dû introduire le concept des arborescences de propriétés bien plus tôt dans le pipeline (lors du pré-paint) et modifier toutes les API qui en dépendent afin qu'elles nécessitent d'être exécutées avant leur exécution.

Cette histoire est un autre aspect du modèle de refactorisation BlinkNG: identifier les calculs clés, refactoriser pour éviter de les dupliquer et créer des étapes de pipeline bien définies qui créent les structures de données qui les alimentent. Nous calculons les arborescences de propriétés au moment exact où toutes les informations nécessaires sont disponibles. De plus, nous nous assurons qu'elles ne peuvent pas changer pendant l'exécution des étapes de rendu ultérieures.

Composite après peinture: peinture pour tuyaux et composition

La superposition consiste à déterminer quel contenu DOM doit être intégré dans sa propre couche composée (qui, à son tour, représente une texture GPU). Avant RenderingNG, la couche s'exécutait avant Paint, et non après (cliquez ici pour voir le pipeline actuel, et notez le changement d'ordre). Nous devons d'abord décider quelles parties du DOM ont été placées dans chaque calque composé, puis dessiner des listes d'affichage pour ces textures. Naturellement, les décisions reposaient sur des facteurs tels que les éléments DOM qui s'animent ou faisaient défiler, ou ceux qui comportaient des transformations 3D, et quels éléments étaient superposés.

Cela posait des problèmes majeurs, car il fallait plus ou moins contenir des dépendances circulaires dans le code, ce qui est un gros problème pour un pipeline de rendu. Voyons pourquoi à travers un exemple. Supposons que nous devions invalidate "Pain" (autrement dit, redessiner la liste d'affichage, puis la tramer à nouveau). La nécessité d'invalider un utilisateur peut être due à une modification du DOM, ou à un changement de style ou de mise en page. Mais bien sûr, nous aimerions n'invalider que les parties qui ont réellement changé. Cela consistait à identifier les couches composées concernées, puis à invalider une partie ou la totalité des listes d'affichage pour ces calques.

En d'autres termes, l'invalidation dépendait des décisions du DOM, du style, de la mise en page et des couches précédentes (passage: signification de l'image affichée précédente). Mais la couche actuelle dépend également de tous ces éléments. Et comme nous n'avions pas deux copies de toutes les données de superposition, il était difficile de faire la différence entre les décisions passées et futures en termes de couches. Nous avons donc fini avec beaucoup de code avec un raisonnement circulaire. Cela entraînait parfois des codes illogiques ou incorrects, voire des plantages ou des problèmes de sécurité, si nous n'étions pas très prudents.

Pour faire face à cette situation, nous avons commencé par présenter le concept d'objet DisableCompositingQueryAsserts. La plupart du temps, si le code essayait d'interroger les décisions de superposition antérieures, cela provoque un échec de l'assertion et fait planter le navigateur s'il était en mode débogage. Cela nous a permis d'éviter d'introduire de nouveaux bugs. Et chaque fois que le code devait légitimement interroger les décisions de superposition antérieures, nous incorporons du code pour l'autoriser en allouant un objet DisableCompositingQueryAsserts.

Au fil du temps, nous avions l'intention de supprimer tous les objets DisableCompositingQueryAssert des sites d'appel, puis de déclarer le code sûr et correct. Mais nous avons découvert qu'un certain nombre d'appels étaient pratiquement impossibles à supprimer tant que la couche se produisait avant l'opération Paint. (Nous n'avons finalement pu le supprimer que très récemment.) C'est la première raison découverte pour le projet Composite After Paint. Nous avons appris que, même si vous avez une phase de pipeline bien définie pour une opération, si elle n'est pas au bon endroit dans le pipeline, vous finirez par être bloqué.

La deuxième raison du projet Composite After Paint était liée au bug Fundamental Composition. Une façon de signaler ce bug est que les éléments DOM ne constituent pas une bonne représentation 1:1 d'un schéma de superposition efficace ou complet pour le contenu d'une page Web. Étant donné que la composition était antérieure à "Paint", elle dépendait plus ou moins intrinsèquement d'éléments DOM, et non de listes d'affichage ou d'arborescences de propriétés. Cette approche est très semblable à la raison pour laquelle nous avons introduit les arbres de propriété. Comme pour les arbres de propriété, la solution disparaît directement si vous trouvez la bonne phase de pipeline, si vous l'exécutez au bon moment et si vous lui fournissez les structures de données clés appropriées. Comme pour les arbres de propriété, c'était une bonne occasion de garantir qu'une fois la phase de peinture terminée, sa sortie est immuable pour toutes les phases suivantes du pipeline.

Avantages

Comme vous l'avez vu, un pipeline de rendu bien défini offre d'énormes avantages à long terme. Il y en a encore plus que vous ne le pensez:

  • Fiabilité nettement améliorée: dans ce cas, il est assez simple. Un code plus clair avec des interfaces bien définies et compréhensibles est plus facile à comprendre, à écrire et à tester. Cela le rend plus fiable. Cela permet également de rendre le code plus sûr et plus stable, avec moins de plantages et de bugs après utilisation après libération.
  • Couverture de test étendue: avec BlinkNG, nous avons ajouté un grand nombre de nouveaux tests à notre suite. Cela inclut des tests unitaires qui fournissent une vérification ciblée des éléments internes, des tests de régression qui nous empêchent de réintroduire d'anciens bugs que nous avons corrigés (et tant de !), et de nombreux ajouts à la suite Web Platform Test publique, gérée de façon collective, que tous les navigateurs utilisent pour évaluer la conformité aux normes Web.
  • Extension plus facile: si un système est divisé en composants clairs, il n'est pas nécessaire de comprendre les autres composants à quelque niveau de détail pour progresser sur le système actuel. Cela permet à chacun d'ajouter plus facilement de la valeur au code de rendu sans avoir à être un expert en la matière, et cela permet également de mieux comprendre le comportement de l'ensemble du système.
  • Performances: l'optimisation des algorithmes écrits en code spaghetti est déjà assez difficile, mais il est presque impossible d'obtenir des résultats encore plus importants, comme le défilement universel avec fils de discussion et les animations, ou les processus et threads pour l'isolation de sites sans un tel pipeline. Le parallélisme peut nous aider à améliorer considérablement les performances, mais c'est aussi extrêmement compliqué.
  • Rendement et confinement: plusieurs nouvelles fonctionnalités rendues possibles par BlinkNG permettent d'exploiter le pipeline d'une manière inédite. Par exemple, que se passe-t-il si nous voulons n'exécuter le pipeline de rendu que jusqu'à l'expiration d'un budget ? Ou ignorer le rendu des sous-arborescences qui ne sont pas pertinentes pour l'utilisateur à l'heure actuelle ? C'est ce que permet la propriété CSS content- visibility. Pourquoi faire en sorte que le style d'un composant dépende de sa mise en page ? Il s'agit des requêtes de conteneur.

Étude de cas: requêtes de conteneur

Les requêtes de conteneur sont une fonctionnalité très attendue des plates-formes Web (c'est depuis des années la première fonctionnalité la plus demandée par les développeurs CSS). Si elle est si géniale, pourquoi n'existe-t-elle pas encore ? En effet, l'implémentation de requêtes de conteneur nécessite une compréhension et un contrôle très minutieux de la relation entre le style et le code de mise en page. Examinons cela de plus près.

Une requête de conteneur permet aux styles qui s'appliquent à un élément de dépendre de la taille disposée d'un ancêtre. Étant donné que la taille de mise en page est calculée lors de la mise en page, nous devons exécuter le recalcul de style après la mise en page, mais le recalcul de style s'exécute avant la mise en page. C'est ce paradoxe entre les œufs et la poule. C'est la raison pour laquelle nous ne pouvions pas implémenter les requêtes de conteneur avant BlinkNG.

Comment résoudre ce problème ? S'agit-il d'une dépendance de pipeline inverse, c'est-à-dire du même problème que celui résolu par des projets tels que "Composite After Paint" ? Pire encore, que se passe-t-il si les nouveaux styles modifient la taille de l'ancêtre ? Cela ne mènera-t-il pas parfois à une boucle infinie ?

En principe, la dépendance circulaire peut être résolue en utilisant la propriété CSS "contains", qui permet à l'affichage en dehors d'un élément de ne pas dépendre de l'affichage dans la sous-arborescence de cet élément. Cela signifie que les nouveaux styles appliqués par un conteneur ne peuvent pas affecter sa taille, car les requêtes de conteneur nécessitent un confinement.

Mais en fait, ce n’était pas suffisant et il était nécessaire d’introduire un type de confinement plus faible que le simple confinement de la taille. En effet, il est courant de vouloir qu'un conteneur de requêtes de conteneur ne puisse être redimensionné que dans une seule direction (généralement une direction) en fonction de ses dimensions intégrées. Nous avons donc ajouté le concept de structuration de taille intégrée. Toutefois, comme vous pouvez le voir dans la très longue note de cette section, il n'a pas été clair depuis longtemps si le confinement de la taille de la ligne de commande était possible.

C'est une chose de décrire le confinement dans le langage de spécification abstrait, et c'est toute une autre chose de l'implémenter correctement. Rappelez-vous que l'un des objectifs de BlinkNG était d'appliquer le principe de confinement aux chemins dans les arbres, qui constituent la logique principale du rendu: lors du balayage d'une sous-arborescence, aucune information ne doit être requise de l'extérieur de la sous-arborescence. En l'occurrence, ce n'était pas exactement un accident, il est beaucoup plus clair et plus facile d'implémenter le confinement CSS si le code de rendu respecte ce principe de confinement.

L'avenir: la composition hors-threading... et plus encore !

Le pipeline de rendu présenté ici est un peu en avance sur l'implémentation actuelle de RenderingNG. Il montre que la couche se trouve en dehors du thread principal, alors qu'elle se trouve actuellement sur le thread principal. Toutefois, ce n'est plus qu'une question de temps : maintenant que le composant Composite After Paint a été expédié, et que la couche se termine après l'opération "Painture".

Pour comprendre pourquoi cela est important et où cela peut mener, nous devons considérer l'architecture du moteur de rendu sous un angle un peu plus élevé. L'un des obstacles les plus durables à l'amélioration des performances de Chromium est le simple fait que le thread principal du moteur de rendu gère à la fois la logique d'application principale (c'est-à-dire l'exécution du script) et l'essentiel de l'affichage. Par conséquent, le thread principal est souvent saturé de tâches, et l'encombrement du thread principal constitue souvent un goulot d'étranglement dans l'ensemble du navigateur.

La bonne nouvelle, c'est que ce n'est pas obligatoire ! Cet aspect de l'architecture de Chromium remonte à l'époque du KHTML, époque à laquelle l'exécution monothread était le principal modèle de programmation. Lorsque les processeurs multicœurs sont devenus courants dans les appareils grand public, l'hypothèse à thread unique était pleinement intégrée à Blink (anciennement WebKit). Cela fait longtemps que nous voulions introduire davantage de threads dans le moteur de rendu, mais cela était tout simplement impossible dans l'ancien système. L'un des principaux objectifs de rendu NG était de se creuser dans ce trou et de permettre de déplacer le travail de rendu, en partie ou en totalité, vers un ou plusieurs autres threads.

Maintenant que BlinkNG est terminé, nous commençons déjà à explorer ce domaine. Non-Blocking Commit est une première entrée en matière pour modifier le modèle de thread du moteur de rendu. La phase de validation du compositeur (ou simplement commit) est une étape de synchronisation entre le thread principal et le thread du compositeur. Lors du commit, nous créons des copies des données de rendu générées sur le thread principal, qui seront utilisées par le code de composition en aval exécuté sur le thread compositeur. Pendant cette synchronisation, l'exécution du thread principal est arrêtée pendant que la copie du code s'exécute sur le thread compositeur. Cela permet de s'assurer que le thread principal ne modifie pas ses données de rendu pendant que le thread compositeur les copie.

Un commit non bloquant élimine la nécessité pour le thread principal de s'arrêter et d'attendre la fin de la phase de commit. Le thread principal continue de travailler pendant que le commit s'exécute simultanément sur le thread compositeur. L'effet net d'un commit non bloquant sera une réduction du temps consacré au rendu des tâches sur le thread principal, ce qui réduira l'encombrement du thread principal et améliorera les performances. Au moment où nous écrivons ces lignes (mars 2022), nous disposons d'un prototype fonctionnel de l'engagement non bloquant, et nous nous préparons à analyser en détail son impact sur les performances.

Dans les ailes, il s'agit d'une composition hors thread principal. L'objectif est de faire correspondre le moteur de rendu à l'illustration en déplaçant la layerization depuis le thread principal vers un thread de nœud de calcul. Comme pour le commit non bloquant, cela permet de réduire l'encombrement du thread principal en diminuant sa charge de travail de rendu. Un tel projet n'aurait jamais été possible sans les améliorations architecturales de Composite After Paint.

Et il y a d'autres projets dans le pipeline (jeu de mots prévu) ! Nous disposons enfin d'une base qui permet de tester la redistribution des opérations de rendu, et nous avons hâte de voir ce qu'il est possible de faire.