Un navigateur moderne est un projet extrêmement complexe dans lequel même des changements d'apparence inoffensive peuvent entraîner des surprises inattendues. Par conséquent, il existe de nombreux tests internes qui devraient détecter ces modifications avant la publication. Il n'y a jamais trop de tests, il est donc utile d'utiliser également des benchmarks publics tiers.
Je m'appelle Andrey Logvinov, je travaille dans le groupe de développement du moteur de rendu Yandex.Browser à Nizhny Novgorod. Aujourd'hui, je vais expliquer aux lecteurs de Habr comment fonctionne la gestion de la mémoire dans le projet Chromium en donnant l'exemple d'un problème mystérieux qui a entraîné une dégradation des performances lors du test du
compteur de vitesse . Ce message est basé sur mon rapport de l'événement Yandex.Inside.

Une fois sur notre tableau de bord de performance, nous avons constaté une détérioration du test du compteur de vitesse. Ce test mesure les performances globales du navigateur sur une application proche de la réalité - une liste de tâches, où le test ajoute des éléments à la liste, puis les raye. Les résultats des tests sont affectés à la fois par les performances du moteur V8 JS et par la vitesse de rendu des pages dans le moteur Blink. Le test du compteur de vitesse se compose de plusieurs sous-tests, où l'application de test est écrite en utilisant l'un des cadres JS populaires, par exemple jQuery ou ReactJS. Le résultat global du test est défini comme la moyenne des résultats pour tous les frameworks, mais le test vous permet de voir les performances de chaque framework individuellement. Il est à noter que le test n'a pas pour but d'évaluer les performances des frameworks, ils ne sont utilisés que pour rendre le test moins synthétique et plus proche des applications web réelles. Le détail par sous-test a montré qu'une détérioration n'est observée que pour la version de l'application de test créée à l'aide de jQuery. Et c'est déjà intéressant, d'accord.
L'enquête sur de telles situations commence de manière assez standard - nous déterminons quel engagement particulier envers le code a provoqué le problème. Pour ce faire, nous stockons les assemblages Yandex.Browser pour chaque (!) Commit au cours des dernières années (il serait difficile de le réassembler, car l'assemblage prend plusieurs heures). Cela prend beaucoup d'espace sur les serveurs, mais cela aide généralement à trouver rapidement la source du problème. Mais cette fois n'a pas fonctionné rapidement. Il s'est avéré que la détérioration des résultats des tests a coïncidé avec un commit intégrant la prochaine version de Chromium. Le résultat n'est pas encourageant, car la nouvelle version de Chromium apporte un grand nombre de changements à la fois.
Comme nous n'avons reçu aucune information indiquant un changement spécifique, j'ai dû faire une étude de fond du problème. Pour ce faire, nous avons utilisé les outils de développement pour supprimer les traces de test. Nous avons remarqué une caractéristique étrange - des intervalles «déchirés» pour l'exécution des fonctions de test Javascript.

Nous supprimons une trace plus technique avec about: tracing et voyons qu'il s'agit de
garbage collection (GC) dans Blink.

La piste mémoire ci-dessous montre que ces pauses GC prennent non seulement beaucoup de temps, mais ne contribuent pas non plus à arrêter la croissance de la consommation de mémoire.

Mais si vous insérez un appel GC explicite dans le test, nous voyons une image complètement différente - la mémoire est conservée dans la région zéro et ne fuit pas. Ainsi, nous n'avons aucune fuite de mémoire et le problème est lié aux fonctionnalités du collecteur. Nous continuons à creuser. Nous démarrons le débogueur et voyons que le garbage collector a contourné environ 500 mille objets! Un tel nombre d'objets n'a pas pu affecter les performances. Mais d'où venaient-ils?
Et ici, nous avons besoin d'un petit flashback sur le dispositif de collecte des ordures dans Blink. Il supprime les objets morts, mais ne déplace pas les objets vivants, ce qui permet de fonctionner avec des pointeurs nus dans des variables locales en code C ++. Ce modèle est activement utilisé dans Blink. Mais il a son propre prix - lors de la collecte des ordures, vous devez
analyser la pile de flux et si quelque chose de similaire à un pointeur vers un objet d'un tas (tas) y est trouvé, alors considérez l'objet et tout ce qu'il fait référence directement ou indirectement comme vivant. Cela conduit au fait que certains objets pratiquement inaccessibles et donc «morts» sont identifiés comme vivants. Par conséquent, cette forme de collecte des ordures est également appelée conservatrice.
Nous vérifions la connexion avec l'analyse de la pile et la sautons. Le problème a disparu.
Qu'est-ce qui peut être tel dans une pile qui contient 500 000 objets? Nous mettons un point d'arrêt dans la fonction d'ajout d'objets - entre autres choses, nous voyons qu'il y a suspect:
blink :: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...
Une référence de table de hachage est probablement suspecte! Nous testons l'hypothèse en sautant l'ajout de ce lien. Le problème a disparu. Eh bien, nous sommes un pas de plus vers la réponse.
Nous rappelons une autre caractéristique du garbage collector dans Blink: s'il voit un pointeur vers l'intérieur de la table de hachage, il considère cela comme un signe d'itération continue sur la table, ce qui signifie qu'il considère tous les liens de cette table utiles et continue de les contourner. Dans notre cas, inactif. Mais quelle fonction est à l'origine de ce lien?
Nous avançons quelques images de la pile plus haut, prenons la position actuelle du scanner, regardons le cadre de la pile dans lequel il tombe. Il s'agit d'une fonction appelée
ScheduleGCIfNeeded . Il semblerait qu'ici il soit le coupable, mais ... nous regardons le code source de la fonction et voyons qu'il n'y a pas du tout de tables de hachage. De plus, cela fait déjà partie du garbage collector lui-même, et il n'a tout simplement pas besoin de faire référence aux objets du tas Blink. D'où vient ce "mauvais" lien?
Nous avons défini un point d'arrêt sur la modification de la cellule mémoire, dans laquelle nous avons trouvé un lien vers la table de hachage. Nous voyons que l'une des fonctions internes appelées V8PerIsolateData :: AddActiveScriptWrappable y écrit. Là, ils ajoutent des éléments HTML créés de certains types, y compris des entrées, à une seule table de hachage active_script_wrappables_. Ce tableau est nécessaire pour empêcher la suppression d'éléments qui ne sont plus référencés depuis Javascript ou l'arborescence DOM, mais qui sont associés à toute activité externe qui, par exemple, peut générer des événements.
Le garbage collector pendant la traversée normale de la table prend en compte l'état des éléments qu'il contient et les marque comme vivants ou ne les marque pas, puis ils sont supprimés à l'étape suivante de l'assemblage. Cependant, dans notre cas, un pointeur vers le stockage interne de cette table apparaît lorsque la pile est analysée et tous les éléments de la table sont marqués comme actifs.
Mais comment la valeur de la pile d'une fonction a-t-elle touché la pile d'une autre?!
Pensez à ScheduleGCIfNeeded. Rappelons que rien d'utile n'a été trouvé dans le code source de cette fonction, mais cela signifie seulement qu'il est temps de descendre à un niveau inférieur et de vérifier le
compilateur . Le prologue démonté de la fonction ScheduleGCIfNeeded ressemble à ceci:
0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax
On peut voir que la fonction
descend en particulier vers 0B8h , et cet endroit n'est plus utilisé. Mais à cause de cela, le scanner de pile voit ce qui était précédemment enregistré par d'autres fonctions. Et par hasard, un pointeur vers l'intérieur de la table de hachage laissée par la fonction AddActiveScriptWrappable pénètre dans ce «trou». Il s'est avéré que la raison de l'apparition d'un «trou» dans ce cas était la
macro de débogage
VLOG à l'intérieur de la fonction, qui affiche des informations supplémentaires dans le journal.
Mais pourquoi la table active_script_wrappable_ avait-elle des centaines de milliers d'éléments? Pourquoi la dégradation des performances est-elle observée uniquement sur le test jQuery? La réponse aux deux questions est la même - dans ce test particulier, pour chaque modification (comme une coche dans la case à cocher), l'interface utilisateur entière est complètement recréée. Le test produit des éléments qui se transforment presque immédiatement en ordures. Les autres tests du compteur de vitesse sont plus prudents et ne créent pas d'éléments inutiles, par conséquent, aucune dégradation des performances n'est observée pour eux. Si vous développez des services Web, vous devez en tenir compte afin de ne pas créer de travail inutile pour le navigateur.
Mais pourquoi le problème ne se posait-il maintenant que si la macro VLOG était antérieure? Il n'y a pas de réponse exacte, mais très probablement, pendant la mise à jour, la position relative des éléments sur la pile a changé, à cause de quoi le pointeur vers la table de hachage est devenu accidentellement accessible au scanner. En fait, nous avons gagné à la loterie. Pour fermer rapidement le «trou» et restaurer les performances, nous avons supprimé la macro de débogage VLOG. Pour les utilisateurs, il est inutile, et pour nos propres besoins de diagnostic, nous pouvons toujours le réactiver. Nous avons également partagé nos expériences avec d'autres développeurs de Chromium. La réponse a confirmé nos craintes: il s'agit d'un problème fondamental de la collecte des ordures conservatrice dans Blink, qui n'a pas de solution systémique.
Liens intéressants
1. Si vous souhaitez en savoir plus sur la vie quotidienne inhabituelle de notre groupe, rappelez-vous l'
histoire du rectangle noir , qui a conduit à l'accélération non seulement de Yandex.Browser, mais de l'ensemble du projet Chromium.
2. Je vous invite également à écouter d'autres reportages lors du prochain événement
Yandex.Inside le 16 février, les inscriptions sont ouvertes et la diffusion le sera également.