Blink désigne la mise en œuvre dans Chromium de la plate-forme Web. Il englobe toutes les phases de l'affichage avant la composition, jusqu'à la commission du compositeur. Pour en savoir plus sur l'architecture de rendu blink, consultez un article précédent de cette série.
Blink a été créé en tant que duplication de WebKit, qui est lui-même une copie de KHTML, qui date de 1998. Il contient certains des codes les plus anciens (et les plus critiques) de Chromium, et en 2014, il avait clairement démontré son ancienneté. Cette année-là, nous nous sommes lancés dans un ensemble de projets ambitieux sous la bannière de ce que nous appelons BlinkNG, dans le but de corriger des lacunes de longue date au niveau de l'organisation et de la structure du code Blink. Cet article explore BlinkNG et ses projets constitutifs: pourquoi nous les avons faits, ce qu'ils ont accompli, les principes directeurs qui ont façonné leur conception et les possibilités d'améliorations futures qu'ils offrent.
Rendu avant NG
Dans Blink, le pipeline de rendu était toujours conceptuellement divisé en plusieurs phases (style, layout, paint, etc.), mais les barrières de l'abstraction étaient percées. De manière générale, les données associées au rendu étaient constituées d'objets modifiables de longue durée. Ces objets peuvent et l'ont été modifiés à tout moment, et étaient fréquemment recyclés et réutilisés par des mises à jour successives de rendu. Il était impossible de répondre de manière 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 « finalité » ?
- Quand pouvez-vous modifier ces données ?
- Quand cet objet sera-t-il supprimé ?
En voici de nombreux exemples:
Le paramètre Style génère des ComputedStyle
à partir des feuilles de style. mais ComputedStyle
n'était pas immuable. Dans certains cas, il est 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 peut même modifier l'arborescence. Il n'y avait pas de séparation nette entre les entrées et les sorties de la mise en page.
Le style générait des structures de données accessoires qui ont déterminé le cours de la composition. Ces structures de données ont été modifiées en place par chaque phase après le style.
À un niveau inférieur, les types de données de rendu sont en grande partie constitués d'arborescences spécialisées (par exemple, l'arborescence DOM, l'arborescence de style, l'arborescence de mise en page ou l'arborescence de propriétés de peinture). et de rendu sont implémentés sous forme de parcours dans des arbres récursifs. Idéalement, un parcours dans les arbres 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 située à la racine de ce nœud. Ce n'était jamais vrai pré-RenderingNG ; parcourt les informations fréquemment consultées des ancêtres du nœud en cours de traitement. Cela rendait le système très fragile et sujet aux erreurs. Il était également impossible de commencer un parcours d'arbre depuis n'importe où, sauf la racine de l'arbre.
Enfin, il y avait de nombreuses rampes dans le pipeline de rendu, disséminées dans tout le code: mises en page forcées déclenchées par JavaScript, mises à jour partielles déclenchées pendant le chargement du document, mises à jour forcées pour préparer le ciblage des é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. Il y avait même quelques chemins récursifs et réentants dans le pipeline de rendu (c'est-à-dire le passage au début d'une étape en partant du milieu d'une autre). Chacune de ces rampes avait son propre comportement idiosyncratique et, dans certains cas, le résultat du rendu dépendrait de la manière dont la mise à jour du rendu était déclenchée.
Ce que nous avons modifié
BlinkNG est composé de nombreux sous-projets, petits ou grands, qui ont tous pour objectif commun d'éliminer les déficits architecturaux décrits précédemment. Ces projets partagent quelques principes directeurs conçus pour transformer le pipeline de rendu en un pipeline réel:
- Uniform point of entry (Point d'entrée uniforme) : le pipeline doit toujours apparaître 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, et les sorties ne doivent dépendre que des entrées définies.
- Entrées constantes: les entrées de toute étape doivent être effectivement constantes pendant l'exécution de la phase.
- Sorties immuables: une fois qu'une étape est terminée, ses sorties doivent rester immuables jusqu'à la fin 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 présenter un état d'autocohérence.
- Déduplication des tâches: ne calculez chaque élément qu'une seule fois.
Une liste complète des sous-projets BlinkNG rendrait la lecture fastidieuse, mais voici quelques-unes des conséquences particulières.
Cycle de vie du document
La classe DocumentLifecycle suit notre progression dans le pipeline de rendu. Elle nous permet d'effectuer des vérifications de base qui appliquent les règles invariantes répertoriées précédemment, par exemple:
- Si nous modifions une propriété ComputedStyle, le cycle de vie du document doit être
kInStyleRecalc
. - Si l'état DocumentLifecycle est
kStyleClean
ou une version ultérieure,NeedsStyleRecalc()
doit renvoyer false pour tous les nœuds associés. - Lorsque vous entrez dans la phase de cycle de vie paint, l'état du cycle de vie doit être
kPrePaintClean
.
Lors de l'implémentation de BlinkNG, nous avons systématiquement éliminé les chemins de code qui ne respectaient pas ces règles invariantes, et nous avons éparpillé beaucoup plus d'assertions dans le code pour éviter toute régression.
S'il vous est déjà arrivé d'étudier un code de rendu de bas niveau, vous vous demandez peut-être comment vous êtes arrivé là. Comme indiqué précédemment, il existe plusieurs 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 à une phase intermédiaire, plutôt que de commencer par le début. Dans le cadre de BlinkNG, nous avons analysé ces chemins d'appel et déterminé qu'ils étaient tous réductibles à deux scénarios de base:
- Toutes les données de rendu doivent être mises à jour (par exemple, lorsque vous générez de nouveaux pixels à afficher ou effectuez un test de positionnement pour le ciblage des événements).
- Nous avons besoin d'une valeur à jour pour une requête spécifique à laquelle il est possible de 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'y a 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é beaucoup de mystères concernant le moment exact et le mode des mises à jour du rendu, ce qui a permis d'analyser beaucoup plus facilement le comportement du système.
Style de tuyauterie, mise en page et pré-peinture
Collectivement, les phases de rendu avant paint sont responsables des éléments suivants:
- Exécuter l'algorithme de cascade de style pour calculer les propriétés de style finales des nœuds DOM
- Génération de l'arborescence de mise en page représentant la hiérarchie en cases du document.
- Détermination des informations de taille et de position pour toutes les boîtes.
- Arrondissement ou ancrage de la géométrie de sous-pixels à des limites de pixels entiers pour la peinture.
- Déterminer les propriétés des couches composites (transformation affine, filtres, opacité ou tout autre élément pouvant être accéléré par GPU)
- Déterminer quel contenu a changé depuis la phase précédente de peinture et qui doit être peint ou repeint (invalidation de peinture).
Cette liste n'a pas changé, mais avant BlinkNG, une grande partie 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 des nœuds, mais dans certains cas particuliers, nous ne déterminions les valeurs des propriétés de style finale qu'une fois la phase style terminée. Il n'y avait pas de point formel ni applicable dans le processus de rendu où nous pouvions dire avec certitude que les informations de style étaient complètes et immuables.
L'invalidation de peinture est un autre exemple de problème pré-BlinkNG. Auparavant, l'invalidation de peinture était étalée lors de toutes les phases de rendu, jusqu'à la peinture. Lors de la modification du code de style ou de mise en page, il était difficile de savoir quelles modifications de la logique d'invalidation des dessins étaient nécessaires, et il était facile de commettre une erreur, ce qui provoquait des bugs d'invalidation ou de sous-invalidation excessive. Pour en savoir plus sur les subtilités de l'ancien système d'invalidation des dessins, consultez l'article de cette série consacrée à LayoutNG.
L'ancrage de la géométrie de mise en page sous-pixels à des limites de pixels entiers pour la peinture est un exemple de cas où nous avions plusieurs implémentations de la même fonctionnalité et avons fait beaucoup de travail redondant. Il y avait un chemin de code d'ancrage des pixels utilisé par le système de peinture et un chemin de code entièrement distinct utilisé chaque fois que nous avions besoin d'un calcul ponctuel et à la volée des coordonnées capturées par pixel en dehors du code de peinture. Il va sans dire que chaque implémentation avait ses propres bugs et que les résultats 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 nuisait aux performances.
Voici quelques projets importants qui ont éliminé les défauts d'architecture des phases de rendu avant la peinture.
Project Squad: intégration de la phase de style
Ce projet a comblé deux déficits principaux dans la phase de stylisation qui empêchaient son pipeline d'être proprement dit:
La phase de style comporte deux sorties principales: ComputedStyle
, qui contient le résultat de l'exécution de l'algorithme CSS en cascade sur l'arborescence DOM. et une arborescence de LayoutObjects
, qui définit l'ordre des opérations pour la phase de mise en page. Sur le plan conceptuel, l'exécution de l'algorithme de cascade doit se faire 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 à scinder ces deux étapes en phases distinctes et séquentielles.
Auparavant, ComputedStyle
n'obtenait pas toujours sa valeur finale lors du recalcul du style. ComputedStyle
a été mis à jour lors d'une phase ultérieure du pipeline. Project Squad a réussi à refactoriser ces chemins de code afin que ComputedStyle
ne soit jamais modifié après la phase de style.
LayoutNG: pipeline de 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 mise en page. Nous ne rendrons pas justice à l’ensemble du projet ici, mais il y a quelques aspects importants pour le projet BlinkNG global:
- Auparavant, la phase de mise en page recevait une arborescence de
LayoutObject
créée par la phase de style, et l'annotait avec des informations de taille et de position. Ainsi, il n'y avait pas de séparation nette 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 ajouté la propriété de confinement à 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 située à la 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 en tant qu'entrée en lecture seule à l'algorithme. - Auparavant, il y avait des cas limites où l'algorithme de mise en page n'était pas strictement fonctionnel: le résultat de l'algorithme dépendait de la mise à jour de mise en page la plus récente. LayoutNG a éliminé ces cas.
La phase de pré-peinture
Auparavant, il n'existait pas de phase formelle de rendu de pré-peinture, mais juste un ensemble d'opérations post-mise en page. La phase de pré-peinture est née de la reconnaissance de quelques fonctions associées qui pourraient être mieux implémentées en tant que balayage systématique de l'arborescence de mise en page une fois la mise en page terminée. et surtout:
- Émettre des invalidations de peinture: il est très difficile d'invalidation des dessins correctement au cours de la mise en page, lorsque les informations sont incomplètes. Il est beaucoup plus facile de se tromper et peut s'avérer très efficace si le processus est divisé en deux processus distincts: lors du style et de la mise en page, le contenu peut être marqué à l'aide d'un simple indicateur booléen comme "nécessite peut-être une invalidation de peinture". Lors de l'arborescence de pré-peinture, nous vérifions ces signalements et émettons des invalidations si nécessaire.
- Générer des arbres de propriétés de peinture: processus décrit plus en détail plus loin.
- Calcul et enregistrement des positions de peinture avec pixels mis à l'écran: 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 aucun calcul redondant.
Arborescences de propriétés: géométrie cohérente
Les arborescences de propriétés ont été introduites au début de RenderingNG pour gérer la complexité du défilement, qui sur le Web présente une structure différente de celle de tous les autres types d'effets visuels. Avant les arborescences de propriétés, le compositeur de Chromium utilisait un seul "calque" pour représenter la relation géométrique des contenus composites, mais cela s'est rapidement effondré lorsque la complexité des éléments géographiques tels que position:fixe est devenue évidente. La hiérarchie des couches a augmenté de pointeurs non locaux supplémentaires, indiquant le "parent de défilement" ou "clip parent" d'une couche et, très vite, il était très difficile de comprendre le code.
Les arborescences de propriétés ont corrigé ce problème en représentant séparément les aspects du défilement et des extraits du contenu de 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, "tous" nous devions implémenter des algorithmes au-dessus des arborescences de propriétés, comme la transformation par espace d'écran des couches composites, ou déterminer quelles couches défilent et lesquelles ne l'ont pas fait.
En fait, nous avons vite remarqué que des questions géométriques similaires étaient soulevées à de nombreux autres endroits du code. 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 ; avaient tous un sous-ensemble différent de bogues ; et aucun d’entre eux n’a modélisé la véritable structure du site Web. La solution s'est ensuite imposée clairement: centraliser tous les algorithmes de géométrie en un seul endroit et refactoriser tout le code pour les utiliser.
Ces algorithmes dépendent tous des arborescences de propriétés. C'est pourquoi elles constituent une structure de données clé, c'est-à-dire utilisée tout au long du pipeline, de RenderingNG. Par conséquent, pour atteindre cet objectif de code géométrique centralisé, nous avons dû introduire le concept d'arborescence de propriétés beaucoup plus tôt dans le pipeline (lors de la pré-peinture) et modifier toutes les API qui en dépendaient pour qu'elles nécessitent l'exécution d'une pré-peinture avant de pouvoir être exécutées.
Cette histoire constitue un autre aspect du modèle de refactorisation BlinkNG: identifiez les calculs clés, refactorisez-les pour éviter de les dupliquer et créez 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. et nous nous assurons que les arborescences de propriétés ne peuvent pas être modifiées pendant l'exécution des étapes de rendu ultérieures.
Composition après peinture composite: peinture par tuyau et composition
La superposition est le processus qui consiste à déterminer quel contenu DOM est intégré dans sa propre couche composite (qui, à son tour, représente une texture GPU). Avant RenderNG, la couche était exécutée avant la peinture, et non après (cliquez ici pour voir le pipeline actuel, et prendre en compte le changement d'ordre). Nous devons d'abord décider quelles parties du DOM sont incluses dans quelles couches composites, puis tracer des listes d'affichage pour ces textures. Naturellement, les décisions dépendaient de facteurs tels que les éléments DOM qui s'animent ou défilent, ou qui comportent des transformations 3D, et sur quels éléments.
Cela entraînait des problèmes majeurs, car il fallait plus ou moins intégrer des dépendances circulaires dans le code, ce qui constitue un gros problème pour un pipeline de rendu. Voyons pourquoi à l'aide d'un exemple. Supposons que nous devions invalider la peinture (ce qui signifie que nous devons redessiner la liste d'affichage, puis la matricier à nouveau). Le besoin d'invalidation peut être dû à une modification du DOM, ou à une modification du style ou de la mise en page. Mais bien sûr, nous souhaitons uniquement invalider les parties qui ont réellement changé. Il a donc fallu identifier les couches composites affectées, puis invalider tout ou partie des listes d'affichage de ces calques.
Cela signifie que l'invalidation dépendait du DOM, du style, de la mise en page et des décisions passées concernant la superposition des couches (passé: c'est-à-dire pour le frame affiché précédent). Mais la structuration 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 matière de superposition. Nous avons donc fini avec beaucoup de code avec un raisonnement circulaire. Cela entraînait parfois un code illogique ou incorrect, 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 abordé très tôt le concept d'objet DisableCompositingQueryAsserts
. La plupart du temps, si le code tentait d'interroger les décisions de superposition antérieures, cela provoquait un échec d'assertion et provoquait le plantage du navigateur s'il était en mode débogage. Cela nous a permis d'éviter l'introduction de nouveaux bugs. Dans chaque cas où le code devait légitimement interroger les décisions de superposition antérieures, nous intégrons du code pour l'autoriser en allouant un objet DisableCompositingQueryAsserts
.
Nous avions l'intention de se débarrasser, au fil du temps, de tous les objets DisableCompositingQueryAssert
des sites d'appel, puis de déclarer le code sécurisé et correct. Mais nous avons découvert qu'un certain nombre d'appels étaient essentiellement impossibles à supprimer tant que les couches se produisaient avant la peinture. (Nous avons finalement pu le supprimer 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 ne se trouve pas au bon endroit dans le pipeline, vous finirez par être bloqué.
La deuxième raison du projet Composite After Paint était le bug de la composition fondamentale. Vous pouvez préciser 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. Et comme la composition était antérieure à la peinture, elle dépendait plus ou moins intrinsèquement des éléments DOM, et non des listes d'affichage ou des arborescences de propriétés. C'est très semblable à la raison pour laquelle nous avons introduit des arborescences de propriétés. Comme pour les arborescences de propriétés, la solution est efficace si vous identifiez la bonne phase de pipeline, exécutez-la au bon moment et fournissez-lui les structures de données clés appropriées. Comme pour les arborescences de propriétés, 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 en existe encore plus que vous ne le pensez:
- Fiabilité nettement améliorée: cette étape est assez simple. Un code plus propre, avec des interfaces bien définies et compréhensibles, est plus facile à comprendre, à écrire et à tester. Cela le rend plus fiable. Il rend également le code plus sûr et plus stable, avec moins de plantages et de bugs d'utilisation après libération.
- Couverture des tests étendue: dans le cadre de BlinkNG, nous avons ajouté un grand nombre de nouveaux tests à notre suite. Cela inclut les tests unitaires qui fournissent une vérification ciblée des composants internes. des tests de régression qui nous empêchent de réintroduire d'anciens bugs que nous avons corrigés (tellement de bugs !) ; et de nombreuses fonctionnalités ajoutées au grand public, la suite Web Platform Test, gérée collectivement, et que tous les navigateurs utilisent pour évaluer la conformité aux normes Web.
- Facilité d'extension: si un système est décomposé en composants clairs, il n'est pas nécessaire de comprendre les autres composants, quel que soit le niveau de détail, pour progresser sur le système actuel. Cela permet à chacun d'ajouter facilement de la valeur au code de rendu sans avoir besoin d'être un expert en la matière, et il est également plus facile de comprendre le comportement de l'ensemble du système.
- Performances: l'optimisation des algorithmes écrits en code spaghetti est assez difficile, mais il est presque impossible d'obtenir des résultats encore plus importants, comme des animations et un défilement universels en fils de discussion, ou des processus et des threads pour l'isolation de sites sans un tel pipeline. Le parallélisme peut nous aider à améliorer considérablement les performances, mais il est aussi extrêmement compliqué.
- Rendement et confinement: BlinkNG a rendu possible plusieurs nouvelles fonctionnalités qui exploitent le pipeline de manière inédite. Par exemple, que se passe-t-il si nous voulons exécuter uniquement le pipeline de rendu jusqu'à l'expiration d'un budget ? Ou ignorer l'affichage des sous-arborescences connues pour ne pas être pertinentes pour l'utilisateur pour le moment ? C'est ce que la propriété CSS content- visibility permet. Comment faire pour 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 conteneurs
Les requêtes de conteneurs sont une fonctionnalité à venir de la plate-forme Web, très attendue. Il s'agit de la fonctionnalité la plus demandée par les développeurs CSS depuis des années. Si c'est génial, pourquoi n'existe-t-il pas encore ? En effet, l'implémentation de requêtes de conteneur nécessite une compréhension et un contrôle très attentifs de la relation entre le style et le code de mise en page. Voyons 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 la mise en page est calculée lors de la mise en page, cela signifie que nous devons exécuter un nouveau calcul du style après la mise en page. mais le recalcul du style s'exécute avant la mise en page. Ce paradoxe de l'œuf et de la poule est la seule raison pour laquelle nous ne pouvions pas implémenter de requêtes de conteneur avant BlinkNG.
Comment pouvons-nous résoudre ce problème ? Ne s'agit-il pas d'une dépendance de pipeline inverse, c'est-à-dire du même problème que des projets tels que Composite After Paint ? Pire encore, que se passe-t-il si les nouveaux styles changent la taille de l'ancêtre ? Cela ne risque-t-il pas parfois de créer 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 sans dépendre du rendu dans la sous-arborescence de cet élément. Cela signifie que les nouveaux styles appliqués par un conteneur ne peuvent pas influer sur la taille de ce dernier, car les requêtes de conteneur nécessitent un confinement.
Mais en réalité, ce n'était pas suffisant et il était nécessaire d'introduire un type de structuration plus faible que le simple confinement de la taille. En effet, il est courant qu'un conteneur de requêtes de conteneur puisse être redimensionné dans une seule direction (généralement un bloc) en fonction de ses dimensions intégrées. C'est pourquoi nous avons ajouté le concept de structuration intégrée. Toutefois, comme vous pouvez le constater dans la très longue note de cette section, il n'a pas été clair pendant longtemps si le confinement de la taille intégrée était possible.
Décrire le confinement en langage de spécification abstraite, c'est une chose, et l'implémenter correctement en est une autre chose. Rappelez-vous que l'un des objectifs de BlinkNG était d'appliquer le principe de confinement aux parcours dans les arbres qui constituent la logique principale de rendu: lors de la traversée d'une sous-arborescence, aucune information ne doit être requise de l'extérieur de la sous-arborescence. En l'occurrence (enfin, il ne s'agit pas exactement d'un accident), il est beaucoup plus clair et plus facile d'implémenter la structuration CSS si le code de rendu respecte le principe de confinement.
Future: composition hors thread principal... et même au-delà !
Le pipeline de rendu présenté ici est en fait un peu en avance sur l'implémentation actuelle de RenderingNG. Il montre que la couche est divisée comme étant hors du thread principal, alors qu'elle se trouve actuellement sur le thread principal. Cependant, ce n'est qu'une question de temps avant que cela ne soit fait, maintenant que le Composite After Paint a été livré et que les couches se font après la peinture.
Pour comprendre pourquoi c'est important et où cela peut mener, nous devons considérer l'architecture du moteur de rendu sous un angle de vue 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 principale de l'application (c'est-à-dire l'exécution du script) et l'essentiel de l'affichage. Par conséquent, le thread principal est souvent saturé de travail, et sa congestion est souvent le goulot d'étranglement dans l'ensemble du navigateur.
La bonne nouvelle, c'est que ce n'est pas nécessaire ! Cet aspect de l'architecture de Chromium remonte à l'époque KHTML, lorsque l'exécution monothread était le modèle de programmation dominant. Lorsque les processeurs multicœurs sont devenus courants dans les appareils grand public, l'hypothèse monothread a été incorporée à Blink (anciennement WebKit). Cela fait longtemps que nous voulions intégrer 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 cette technologie était de creuser ce trou et de permettre le déplacement du travail de rendu, en partie ou en totalité, vers un autre thread (ou threads).
Maintenant que BlinkNG est sur le point d'être terminé, nous commençons déjà à explorer ce domaine. Le commit de non-blocage est une première incursion dans la modification du modèle de thread du moteur de rendu. Le commit du compositeur (ou tout simplement le 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 du compositeur. Pendant la synchronisation, le thread principal s'arrête pendant que le code de copie s'exécute sur le thread du compositeur. Cela permet de s'assurer que le thread principal ne modifie pas ses données de rendu pendant que le thread du compositeur les copie.
Avec un commit sans blocage, il n'est plus nécessaire d'arrêter le thread principal et d'attendre la fin de l'étape de commit. Le thread principal continue de travailler pendant que le commit s'exécute simultanément sur le thread compositeur. L'effet net du commit non bloquant sera une réduction du temps consacré au rendu du travail sur le thread principal, ce qui réduira l'encombrement sur le thread principal et améliorera les performances. Au moment où nous écrivons ces lignes (mars 2022), nous disposons d'un prototype fonctionnel du commit de non-blocage, et nous nous préparons à effectuer une analyse détaillée de son impact sur les performances.
En parallèle, la composition en dehors du thread principal est en attente. Elle vise à faire correspondre le moteur de rendu à l'illustration en déplaçant la superposition du thread principal vers un thread de nœud de calcul. Comme le commit de non-blocage, cela réduit l'encombrement sur le thread principal en diminuant la 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 plus de projets dans le pipeline (jeu de mots). Nous disposons enfin d'une base qui nous permet d'expérimenter la redistribution du travail d'affichage, et nous avons hâte de découvrir les possibilités.