Présentation de l'architecture RenderingNG

Chris Harrelson
Chris Harrelson

Dans un article précédent, j'ai présenté les objectifs de l'architecture RenderingNG et les principales propriétés. Cet article explique comment les éléments qui le composent sont configurés et comment le pipeline de rendu les traverse.

En commençant par le niveau le plus élevé et en affichant le détail à partir de là, les tâches d'affichage sont les suivantes:

  1. Affichez le contenu en pixels à l'écran.
  2. Animer des effets visuels sur le contenu d'un état à un autre
  3. Scroll (Défilement) en réponse à l'entrée.
  4. Acheminez efficacement les entrées aux bons endroits pour que les scripts de développement et les autres sous-systèmes puissent y répondre.

Le contenu à afficher est une arborescence de cadres pour chaque onglet du navigateur, ainsi que l'interface utilisateur du navigateur. Un flux d'événements d'entrée bruts provenant d'écrans tactiles, de souris, de claviers et d'autres périphériques matériels.

Chaque image inclut les éléments suivants:

  • État DOM
  • CSS
  • Canevas
  • Ressources externes, telles que des images, des vidéos, des polices et SVG

Un frame correspond à un document HTML et à son URL. Une page Web chargée dans un onglet de navigateur comporte un cadre de premier niveau, des cadres enfants pour chaque iFrame inclus dans le document de premier niveau, ainsi que leurs descendants iFrame récursifs.

Un effet visuel est une opération graphique appliquée à un bitmap, telle que le défilement, la transformation, le rognage, le filtre, l'opacité ou la fusion.

Composants de l'architecture

Dans RenderingNG, ces tâches sont réparties logiquement en plusieurs étapes et composants de code. Les composants se retrouvent dans différents processus du processeur, threads et sous-composants au sein de ces threads. Chacune d'elles joue un rôle important pour assurer la fiabilité, des performances évolutives et l'extensibilité de tout le contenu Web.

Structure du pipeline de rendu

Schéma du pipeline de rendu, comme expliqué dans le texte suivant.

Le rendu s'effectue dans un pipeline qui compte un certain nombre d'étapes et d'artefacts créés au cours du processus. Chaque étape représente un code qui exécute une tâche bien définie au sein du rendu. Les artefacts sont des structures de données qui sont les entrées ou les sorties des phases. Dans le schéma, les entrées ou les sorties sont indiquées par des flèches.

Cet article de blog n'entrera pas dans les détails importants des artefacts. Nous y reviendrons dans le prochain article : Principales structures de données et rôles dans RenderingNG.

Les étapes du pipeline

Dans le schéma précédent, les étapes sont indiquées par des couleurs indiquant dans quel thread ou processus elles s'exécutent:

  • Vert:thread principal du processus de rendu
  • Jaune:compositeur du processus de rendu
  • Orange:processus de visualisation

Dans certains cas, elles peuvent s'exécuter à plusieurs endroits, en fonction des circonstances. C'est pourquoi certains ont deux couleurs.

Les étapes sont les suivantes:

  1. Animer:modifiez les styles calculés et modifiez les arborescences de propriétés au fil du temps en fonction de chronologies déclaratives.
  2. Style:appliquez le CSS au DOM et créez des styles calculés.
  3. Layout (Mise en page) : déterminez la taille et la position des éléments DOM à l'écran, puis créez l'arborescence de fragments immuable.
  4. Pré-peinture:calculez les arborescences de propriétés et invalidate les listes d'affichage et les tuiles de texture GPU existantes, le cas échéant.
  5. Défilement:mettez à jour le décalage de défilement des documents et des éléments DOM à faire défiler, en modifiant les arborescences de propriétés.
  6. Paint:calculez une liste d'affichage décrivant comment matricier les tuiles de texture GPU à partir du DOM.
  7. Commit:copie les arborescences de propriétés et la liste d'affichage dans le thread du compositeur.
  8. Couche:divisez la liste d'affichage en liste de calques composées pour une rastérisation et une animation indépendantes.
  9. Worklets de trame, de décodage et de peinture:transformez respectivement les listes d'affichage, les images encodées et le code du worklet de peinture en tuiles de texture GPU.
  10. Activate (Activer) : crée un frame compositeur représentant comment dessiner et positionner des tuiles GPU à l'écran, ainsi que les éventuels effets visuels.
  11. Agrégation:combine les frames du compositeur de toutes les trames du compositeur visibles en une seule image compositeur globale.
  12. Dessiner:exécute le frame compositeur agrégé sur le GPU pour créer des pixels à l'écran.

Les étapes du pipeline de rendu peuvent être ignorées si elles ne sont pas nécessaires. Par exemple, les animations d'effets visuels et le défilement peuvent ignorer la mise en page, le pré-peinture et le rendu. C'est pourquoi l'animation et le défilement sont indiqués par des points jaunes et verts dans le diagramme. Si la mise en page, le pré-peinture et la peinture peuvent être ignorées pour les effets visuels, elles peuvent être exécutées entièrement sur le thread compositeur et ignorer le thread principal.

Le rendu de l'UI du navigateur n'est pas représenté directement ici, mais peut être considéré comme une version simplifiée de ce même pipeline (et son implémentation partage une grande partie du code). La vidéo (également non représentée directement) s'affiche généralement via un code indépendant qui décode les images en tuiles de texture GPU, qui sont ensuite branchées sur les frames du compositeur et l'étape de dessin.

Structure des processus et des threads

Processus du processeur

L'utilisation de plusieurs processus de processeur permet d'isoler les performances et la sécurité entre les sites et de l'état du navigateur, et d'isoler la stabilité et la sécurité du matériel GPU.

Schéma des différentes parties des processus du processeur

  • Le processus de rendu affiche, anime, fait défiler et achemine les entrées pour une seule combinaison site/onglet. Il existe de nombreux processus de rendu.
  • Le processus du navigateur affiche, anime et achemine les entrées pour l'interface utilisateur du navigateur (y compris la barre d'URL, les titres d'onglets et les icônes), et achemine toutes les entrées restantes vers le processus de rendu approprié. Il n'y a qu'un seul processus de navigateur.
  • Le processus de visualisation regroupe la composition de plusieurs processus de rendu ainsi que le processus du navigateur. Elle effectue un rastérisation et dessine à l'aide du GPU. Il y a exactement un processus de visualisation.

Chaque site se retrouve toujours dans des processus de rendu différents. (En réalité, elles sont toujours sur ordinateur, si possible sur mobile. J'écrirai « toujours » ci-dessous, mais cette mise en garde s’applique partout.)

Plusieurs onglets ou fenêtres de navigateur d'un même site vont généralement dans des processus d'affichage différents, sauf si les onglets sont liés (l'un s'ouvrant l'autre). Lorsque la mémoire est forte, Chromium peut placer plusieurs onglets du même site dans le même processus d'affichage, même s'ils ne sont pas liés.

Dans un même onglet de navigateur, les images de différents sites sont toujours dans des processus de rendu différents, mais les images du même site sont toujours dans le même processus de rendu. Du point de vue de l'affichage, l'utilisation de plusieurs processus de rendu présente un avantage important : les iFrames et les onglets intersites assurent une isolation des performances les uns des autres. De plus, il est possible d'activer davantage d'isolation pour les origines.

Il n'y a qu'un seul processus de visualisation dans Chromium. Après tout, il n'y a généralement qu'un seul GPU et un seul écran pour dessiner. La séparation de Visual Viz dans son propre processus est bonne pour la stabilité face aux bugs dans les pilotes de GPU ou le matériel. Il convient également à l'isolation de sécurité, ce qui est important pour les API GPU telles que Vulkan. Cela est également important pour la sécurité en général.

Étant donné que le navigateur peut comporter de nombreux onglets et fenêtres, et qu'ils ont tous des pixels d'interface utilisateur à dessiner, vous vous demandez peut-être pourquoi il n'y a qu'un seul processus de navigateur. En effet, un seul d'entre eux est sélectionné à la fois. En fait, les onglets de navigateur non visibles sont pour la plupart désactivés et suppriment toute la mémoire GPU. Cependant, des fonctionnalités complexes de rendu d'interface utilisateur de navigateur sont également de plus en plus implémentées dans les processus de rendu (appelées WebUI). Il ne s'agit pas d'un problème d'isolation des performances, mais de la facilité d'utilisation du moteur de rendu Web de Chromium.

Sur les appareils Android plus anciens, le rendu et le processus du navigateur sont partagés lorsqu'ils sont utilisés dans une WebView (cela ne s'applique pas à Chromium sur Android en général, mais uniquement à WebView). Dans WebView, le processus du navigateur est également partagé avec l'application d'intégration, et WebView n'a qu'un seul processus de rendu.

Parfois, un processus utilitaire permet de décoder un contenu vidéo protégé. Ce processus n'est pas représenté ci-dessus.

Fils de discussion

Les threads permettent d'isoler les performances et d'améliorer la réactivité malgré la lenteur des tâches, la parallélisation du pipeline et la mise en mémoire tampon multiple.

Schéma du processus de rendu tel que décrit dans l'article.

  • Le thread principal exécute des scripts, la boucle d'événements de rendu, le cycle de vie du document, les tests de positionnement, la distribution des événements de script et l'analyse du code HTML, CSS et d'autres formats de données.
    • Les outils d'aide à l'exécution de threads principaux effectuent des tâches telles que la création de bitmaps et d'objets blob d'image qui nécessitent un encodage ou un décodage.
    • Les travailleurs Web exécutent un script et une boucle d'événements de rendu pour OffscreenCanvas.
  • Le thread compositeur traite les événements d'entrée, effectue le défilement et les animations du contenu Web, calcule la superposition optimale du contenu Web, et coordonne le décodage des images, les Worklets de peinture et les tâches matricielles.
    • Les assistants de thread compositeur coordonnent les tâches matricielles Viz et exécutent des tâches de décodage d'images, des Worklets de peinture et des trames de remplacement.
  • Les threads de sortie multimédia, démuxer ou audio décodent, traitent et synchronisez des flux vidéo et audio. N'oubliez pas que la vidéo s'exécute en parallèle avec le pipeline de rendu principal.

Il est essentiel de séparer le thread principal du thread compositeur pour isoler les performances de l'animation et du défilement par rapport au thread principal.

Il n'y a qu'un seul thread principal par processus de rendu, même si plusieurs onglets ou frames du même site peuvent se retrouver dans le même processus. Toutefois, les performances sont isolées par rapport au travail effectué dans diverses API de navigateur. Par exemple, la génération de bitmaps et d'objets blob d'image dans l'API Canvas s'exécute dans un thread d'assistance de thread principal.

De même, il n'y a qu'un seul thread compositeur par processus de rendu. Le fait qu'il n'y ait qu'un seul problème ne pose généralement pas de problème, car toutes les opérations très coûteuses sur le thread du compositeur sont déléguées aux threads de travail du compositeur ou au processus de visualisation, et ce travail peut être effectué en parallèle avec le routage d'entrée, le défilement ou l'animation. Les threads de travail compositeurs coordonnent les tâches qui s'exécutent dans le processus de visualisation, mais l'accélération du GPU partout peut échouer pour des raisons indépendantes de la volonté de Chromium, telles que des bugs au niveau du pilote. Dans ces situations, le thread de travail effectue le travail en mode de secours sur le processeur.

Le nombre de threads de travail compositeurs dépend des capacités de l'appareil. Par exemple, les ordinateurs de bureau utilisent généralement plus de threads, car ils ont plus de cœurs de processeur et sont moins limités en termes de batterie que les appareils mobiles. Il s'agit d'un exemple de scaling à la hausse ou à la baisse.

Il est également intéressant de noter que l'architecture de thread du processus de rendu est une application de trois modèles d'optimisation différents:

  • Threads d'assistance:envoi de tâches secondaires de longue durée à des threads supplémentaires, pour que le thread parent reste réactif aux autres requêtes exécutées simultanément. Les threads principaux de l'assistant de thread et de l'assistant de compositeur sont de bons exemples de cette technique.
  • Mise en mémoire tampon multiple:affichage du contenu déjà affiché pendant l'affichage du nouveau contenu, pour masquer la latence d'affichage. Le thread compositeur utilise cette technique.
  • Parallélisation du pipeline:exécution simultanée du pipeline de rendu à plusieurs endroits. C'est ainsi que le défilement et l'animation peuvent être rapides, même en cas de mise à jour du rendu du thread principal, car le défilement et l'animation peuvent s'exécuter en parallèle.

Processus du navigateur

Schéma d'un processus de navigateur montrant la relation entre le thread de rendu et de composition, et l'assistant de thread de composition et de rendu.

  • Le thread de rendu et de composition répond aux entrées dans l'interface utilisateur du navigateur, achemine les autres entrées vers le processus de rendu approprié, met en page et peint l'interface utilisateur du navigateur.
  • Les outils d'aide à la création de threads de rendu et de composition exécutent des tâches de décodage d'images et une trame de remplacement ou un décodage.

Le thread de rendu et de composition du processus du navigateur est semblable au code et aux fonctionnalités d'un processus de rendu, sauf que le thread principal et le thread compositeur sont combinés en un seul. Dans ce cas, un seul thread est nécessaire, car il n'est pas nécessaire d'isoler les performances des longues tâches du thread principal, car il n'y en a pas par conception.

Processus de visualisation

Schéma illustrant que le processus de visualisation inclut le thread principal du GPU et le thread du compositeur d'affichage.

  • Les trames du thread principal du GPU affichent les listes et les images vidéo dans des tuiles de texture GPU, et affichent des images compositeurs à l'écran.
  • Le thread du compositeur d'affichage agrège et optimise la composition de chaque processus de rendu, plus le processus du navigateur, dans un seul frame compositeur pour la présentation à l'écran.

Les trames et les dessins se produisent généralement sur le même thread, car ils reposent tous deux sur des ressources GPU. Il est donc difficile d'effectuer une utilisation multithread du GPU de manière fiable (un accès multithread plus facile au GPU est l'une des motivations pour développer la nouvelle norme Vulkan). Sur Android WebView, il existe un thread de rendu distinct au niveau de l'OS pour le dessin, en raison de la manière dont les WebView sont intégrées à une application native. D'autres plates-formes auront probablement un thread de ce type à l'avenir.

Le compositeur d'affichage se trouve sur un thread différent, car il doit être responsif à tout moment et ne pas bloquer les sources possibles de ralentissement sur le thread principal du GPU. Les appels à du code autre que Chromium, tels que les pilotes de GPU spécifiques aux fournisseurs, peuvent être lents en raison de moyens difficiles à prédire.

Structure des composants

Dans chaque thread principal ou compositeur, des composants logiciels logiques interagissent les uns avec les autres de manière structurée.

Composants du thread principal du processus de rendu

Schéma du moteur de rendu Blink.

  • Moteur de rendu Blink:
    • Le fragment d'arborescence de cadres locaux représente l'arborescence des cadres locaux et le DOM dans les cadres.
    • Le composant API DOM et Canvas contient des implémentations de toutes ces API.
    • L'exécuteur de cycle de vie des documents exécute les étapes du pipeline de rendu jusqu'à l'étape de commit (incluse).
    • Le composant de test des appels d'événement d'entrée et de distribution exécute des tests de positionnement pour identifier l'élément DOM ciblé par un événement. Il exécute également les algorithmes de distribution des événements d'entrée et les comportements par défaut.
  • Le planificateur et l'exécuteur de boucle d'événements de rendu déterminent ce qui doit s'exécuter dans la boucle d'événements, et quand. Elle planifie le rendu à une cadence correspondant à l'écran de l'appareil.

Schéma de l'arborescence des frames.

Les fragments d'arborescence de cadres locaux sont un peu compliqués à prendre en compte. Rappelez-vous qu'une arborescence de cadres correspond à la page principale et à ses iFrame enfants, de manière récursive. Un frame est local dans un processus de rendu s'il est affiché dans ce processus. Sinon, il est remote.

Vous pouvez imaginer colorer les cadres en fonction de leur processus de rendu. Dans l'image précédente, les cercles verts représentent toutes les images d'un même processus de rendu ; les cercles orange dans une seconde et les bleus dans un troisième.

Un fragment d'arborescence de cadres local est un composant connecté de même couleur dans une arborescence de cadres. L'image comprend quatre arborescences locales: deux pour le site A, un pour le site B et un pour le site C. Chaque arborescence de frames local dispose de son propre composant de moteur de rendu Blink. Le moteur de rendu Blink d'une arborescence de frames local peut ou non se trouver dans le même processus de rendu que d'autres arborescences de frames locaux (il est déterminé par la manière dont les processus de rendu sont sélectionnés, comme décrit précédemment).

Structure des threads du compositeur du processus de rendu

Schéma illustrant les composants du compositeur du processus de rendu.

Les composants du compositeur du processus de rendu incluent:

  • Un gestionnaire de données qui gère une liste de calques composée, affiche des listes et des arborescences de propriétés.
  • Un exécuteur de cycle de vie qui exécute les étapes d'animation, de défilement, composite, matricielle, de décodage et d'activation du pipeline de rendu. (N'oubliez pas que l'animation et le défilement peuvent se produire à la fois dans le thread principal et dans le compositeur.)
  • Un gestionnaire de test d'entrée et de positionnement effectue le traitement des entrées et des tests de positionnement à la résolution des couches composées, pour déterminer si des gestes de défilement peuvent être exécutés sur le thread compositeur, et pour déterminer quels tests de positionnement du processus de rendu doivent cibler.

Exemple en pratique

Faisons maintenant concrètement l'architecture à l'aide d'un exemple. Cet exemple comporte trois onglets:

Onglet 1: foo.com

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe  id=two src="bar.com"></iframe>
</html>

Onglet 2: bar.com

<html>
 …
</html>

Onglet 3: baz.com html <html> … </html>

La structure des processus, des threads et des composants de ces onglets se présente comme suit:

Schéma du processus pour les onglets.

Voyons maintenant un exemple pour chacune des quatre tâches principales du rendu, qui, comme vous vous en souvenez, sont:

  1. Rendu le contenu en pixels à l'écran.
  2. Animer des effets visuels sur le contenu d'un état à un autre
  3. Scroll (Défilement) en réponse à l'entrée.
  4. Acheminez efficacement les entrées aux bons endroits pour que les scripts de développement et les autres sous-systèmes puissent y répondre.

Pour afficher le DOM modifié pour le premier onglet:

  1. Un script de développeur modifie le DOM dans le processus de rendu pour foo.com.
  2. Le moteur de rendu Blink indique au compositeur qu'il a besoin d'un rendu.
  3. Le compositeur indique à Viz qu'un rendu doit être effectué.
  4. Visualisation indique au compositeur le début du rendu.
  5. Le compositeur transmet le signal de début au moteur de rendu Blink.
  6. L'exécuteur de boucle d'événements de thread principal exécute le cycle de vie du document.
  7. Le thread principal envoie le résultat au thread compositeur.
  8. L'exécuteur de boucle d'événements du compositeur exécute le cycle de vie de la composition.
  9. Toutes les tâches matricielles sont envoyées à Visualisation pour la trame (il y en a souvent plusieurs).
  10. Visualisation du contenu en trame sur le GPU
  11. La visualisation confirme que la tâche matricielle est terminée. Remarque: Chromium n'attend souvent pas la fin de la trame. Il utilise un jeton de synchronisation qui doit être résolu par des tâches matricielles avant l'exécution de l'étape 15.
  12. Une trame compositeur est envoyée à Visualisation.
  13. Viz regroupe les frames du compositeur pour le processus de rendu de foo.com, celui de l'iFrame bar.com et celui de l'interface utilisateur du navigateur.
  14. Visualisation planifie un tirage au sort.
  15. Viz dessine la trame du compositeur agrégé à l'écran.

Pour animer une transition CSS "transform" dans le deuxième onglet:

  1. Le thread compositeur du processus de rendu bar.com coche une animation dans sa boucle d'événements du compositeur en modifiant les arborescences de propriétés existantes. Le cycle de vie du compositeur est ensuite réexécuté. (Des tâches matricielles et de décodage peuvent être exécutées, mais elles ne sont pas représentées ici.)
  2. Une trame compositeur est envoyée à Visualisation.
  3. Viz regroupe les frames du compositeur pour le processus de rendu de foo.com, celui de bar.com et celui de l'interface utilisateur du navigateur.
  4. Visualisation planifie un tirage au sort.
  5. Viz dessine la trame du compositeur agrégé à l'écran.

Pour faire défiler la page Web au niveau du troisième onglet:

  1. Une séquence d'événements input (souris, appui ou clavier) intervient dans le processus du navigateur.
  2. Chaque événement est acheminé vers le thread du compositeur du processus de rendu de baz.com.
  3. Le compositeur détermine si le thread principal doit être informé de l'événement.
  4. L'événement est envoyé, si nécessaire, au thread principal.
  5. Le thread principal déclenche des écouteurs d'événements input (pointerdown, touchstar, pointermove, touchmove ou wheel) pour voir si les écouteurs appellent preventDefault sur l'événement.
  6. Le thread principal indique si preventDefault a été appelé auprès du compositeur.
  7. Sinon, l'événement d'entrée est renvoyé au processus du navigateur.
  8. Le processus du navigateur le convertit en geste de défilement en le combinant avec d'autres événements récents.
  9. Le geste de défilement est à nouveau envoyé au thread du compositeur du processus de rendu de baz.com,
  10. Le défilement y est appliqué, et le thread compositeur pour le processus de rendu bar.com coche une animation dans sa boucle d'événements du compositeur. Cette opération modifie ensuite le décalage de défilement dans les arborescences de propriétés et réexécute le cycle de vie du compositeur. Il indique également au thread principal de déclencher un événement scroll (non représenté ici).
  11. Une trame compositeur est envoyée à Visualisation.
  12. Viz regroupe les frames du compositeur pour le processus de rendu de foo.com, celui de bar.com et celui de l'interface utilisateur du navigateur.
  13. Visualisation planifie un tirage au sort.
  14. Viz dessine la trame du compositeur agrégé à l'écran.

Pour acheminer un événement click sur un lien hypertexte dans l'iFrame 2 du premier onglet:

  1. Un événement input (souris, appui ou clavier) arrive dans le processus du navigateur. Elle effectue un test de positionnement approximatif pour déterminer si le processus de rendu de l'iFrame bar.com doit recevoir le clic, puis l'envoie à cet endroit.
  2. Le thread compositeur pour bar.com achemine l'événement click vers le thread principal pour bar.com et planifie une tâche de boucle d'événements de rendu pour le traiter.
  3. Le processeur d'événements d'entrée pour les tests de hit de thread principal de bar.com permet de déterminer quel élément DOM dans l'iFrame a fait l'objet d'un clic et déclenche un événement click pour que les scripts puissent l'observer. Si l'icône preventDefault n'est pas détectée, le lien redirige vers le lien hypertexte.
  4. Lors du chargement de la page de destination du lien hypertexte, le nouvel état est affiché, selon une procédure semblable à l'exemple "Afficher le DOM modifié" ci-dessus. Ces modifications ultérieures ne sont pas décrites ici.

Conclusion

Ouf, c'était beaucoup de détails. Comme vous pouvez le voir, l'affichage dans Chromium est assez compliqué. La mémorisation et l'intériorisation de tous les éléments peuvent prendre beaucoup de temps. Par conséquent, ne vous inquiétez pas si cela vous semble accablant.

Le point le plus important à retenir est qu'il existe un pipeline de rendu simple d'un point de vue conceptuel, qui a été divisé en plusieurs composants autonomes grâce à une modularisation minutieuse et une attention aux détails. Ces composants ont ensuite été répartis en processus et threads parallèles pour maximiser les opportunités d'évolutivité et d'extensibilité.

Chacun de ces composants joue un rôle essentiel pour offrir toutes les performances et les fonctionnalités dont les applications Web modernes ont besoin. Nous publierons bientôt des présentations détaillées de chacun d'eux et des rôles qu'ils jouent.

Mais avant cela, j'expliquerai également en quoi les principales structures de données mentionnées dans cet article (celles indiquées en bleu sur les côtés du schéma du pipeline de rendu) sont tout aussi importantes pour RenderingNG que les composants de code.

Merci de votre attention et à bientôt !

Illustrations d'Una Kravets