Les développeurs Web s'attendent à un impact minime, voire nul sur les performances lors du débogage de leur code. Toutefois, cette attente n'est en aucun cas universelle. Un développeur C++ ne s'attendrait jamais à ce qu'une version de débogage de son application atteigne les performances de production. Au début de Chrome, l'ouverture de DevTools avait un impact significatif sur les performances de la page.
Cette dégradation des performances n'est plus ressentie grâce à des années d'investissement dans les fonctionnalités de débogage de DevTools et de V8. Néanmoins, nous ne pourrons jamais réduire à zéro l'impact sur les performances des outils de développement. Définir des points d'arrêt, parcourir le code, collecter des traces de pile, capturer une trace de performances, etc., ont tous un impact sur la vitesse d'exécution à des degrés divers. Après tout, observer quelque chose le modifie.
Toutefois, bien entendu, les coûts indirects de DevTools (comme pour tout débogueur) doivent être raisonnables. Nous avons récemment constaté une augmentation significative du nombre de signalements indiquant que, dans certains cas, DevTools ralentissait l'application à un point tel qu'elle n'était plus utilisable. Vous trouverez ci-dessous une comparaison côte à côte du rapport chromium:1069425, qui illustre le coût supplémentaire en termes de performances lié à l'ouverture des outils pour les développeurs.
Comme vous pouvez le voir dans la vidéo, le ralentissement est de l'ordre de 5 à 10 fois, ce qui n'est clairement pas acceptable. La première étape a consisté à comprendre où tout le temps était passé et ce qui provoquait ce ralentissement massif lorsque DevTools était ouvert. L'utilisation de Linux perf sur le processus du moteur de rendu Chrome a révélé la distribution suivante de la durée d'exécution globale du moteur de rendu :
Nous nous attendions à voir quelque chose en rapport avec la collecte des traces de pile, mais nous ne nous attendions pas à ce que 90 % de la durée d'exécution globale soit consacré à la symbolisation des frames de pile. La symbolisation fait ici référence à l'acte de résoudre les noms de fonctions et les positions sources concrètes (numéros de ligne et de colonne dans les scripts) à partir de blocs de pile bruts.
Inférence du nom de la méthode
Le plus surprenant est que la fonction JSStackFrame::GetMethodName()
de la version 8 concerne presque tout le temps, bien que nous sachions d'après nos précédentes enquêtes que JSStackFrame::GetMethodName()
connaît bien les problèmes de performances. Cette fonction tente de calculer le nom de la méthode pour les frames considérés comme des appels de méthode (frames qui représentent des appels de fonction de la forme obj.func()
plutôt que func()
). Un examen rapide du code a révélé qu'il fonctionne en effectuant une traversée complète de l'objet et de sa chaîne de prototypes, et en recherchant
- les propriétés de données dont l'élément
value
correspond à la route ferméefunc
; ou - Propriétés d'accesseur où
get
ouset
est égal à la fermeturefunc
.
Bien que cela ne semble pas particulièrement bon marché, cela ne semble pas non plus expliquer ce ralentissement horrible. Nous avons donc commencé à examiner l'exemple signalé dans chromium:1069425 et avons constaté que les traces de la pile étaient collectées pour les tâches asynchrones ainsi que pour les messages de journal provenant de classes.js
, un fichier JavaScript de 10 Mo. Un examen plus approfondi a révélé qu'il s'agissait essentiellement d'un environnement d'exécution Java et d'un code d'application compilé en JavaScript. Les traces de la pile contenaient plusieurs frames avec des méthodes appelées sur un objet A
. Nous avons donc pensé qu'il était utile de comprendre de quel type d'objet il s'agissait.
Apparemment,le compilateur Java-JavaScript générait un seul objet contenant un nombre impressionnant de 82 203 fonctions ; ce qui commençait clairement à devenir intéressant. Nous sommes ensuite revenus à JSStackFrame::GetMethodName()
de V8 pour voir s'il y avait des améliorations faciles à mettre en place.
- Il commence par rechercher le
"name"
de la fonction en tant que propriété de l'objet, puis vérifie que la valeur de la propriété correspond à la fonction. - Si la fonction n'a pas de nom ou si l'objet ne possède pas de propriété correspondante, une recherche inverse est effectuée en parcourant toutes les propriétés de l'objet et de ses prototypes.
Dans notre exemple, toutes les fonctions sont anonymes et ont des propriétés "name"
vides.
A.SDV = function() {
// ...
};
La première constatation est que la recherche inverse était divisée en deux étapes (effectuée pour l'objet lui-même et pour chaque objet de sa chaîne de prototypes) :
- extraire les noms de toutes les propriétés énumérables ;
- Effectuez une recherche de propriété générique pour chaque nom, en vérifiant si la valeur de propriété résultante correspond à la route fermée que nous recherchions.
Cela ressemblait à un résultat relativement simple, puisque pour extraire les noms, il faut déjà passer en revue toutes les propriétés. Au lieu d'effectuer les deux passes (O(N) pour l'extraction des noms et O(N log(N)) pour les tests, nous pourrions tout faire en une seule fois et vérifier directement les valeurs des propriétés. L'ensemble de la fonction a ainsi été 2 à 10 fois plus rapide.
Le deuxième résultat était encore plus intéressant. Bien que les fonctions soient techniquement des fonctions anonymes, le moteur V8 a néanmoins enregistré ce que nous appelons un nom inféré pour elles. Pour les littéraux de fonction qui apparaissent à droite des affectations sous la forme obj.foo = function() {...}
, l'analyseur V8 mémorise "obj.foo"
en tant que nom inféré pour le littéral de fonction. Dans notre cas, cela signifie donc que nous ne disposions pas du nom propre à rechercher, mais que nous avions quelque chose d'assez proche: pour l'exemple A.SDV = function() {...}
ci-dessus, nous avions "A.SDV"
comme nom déduit, et nous pourrions obtenir le nom de la propriété à partir du nom déduit en recherchant le dernier point, puis rechercher la propriété "SDV"
sur l'objet. Cela a fonctionné dans la quasi-totalité des cas, en remplaçant une traversée complète coûteuse par une recherche de propriété unique. Ces deux améliorations ont été intégrées dans cette CL et ont permis de réduire considérablement le ralentissement pour l'exemple présenté dans chromium:1069425.
Error.stack
Nous aurions pu le nommer "un jour" ici. Mais quelque chose n'allait pas, car DevTools n'utilise jamais le nom de la méthode pour les frames de pile. En fait, la classe v8::StackFrame
de l'API C++ n'offre même pas de moyen d'accéder au nom de la méthode. Il semblait donc incorrect que nous appelions JSStackFrame::GetMethodName()
en premier lieu. Le seul endroit où nous utilisons (et exposons) le nom de la méthode est l'API JavaScript de trace de la pile. Pour comprendre cette utilisation, prenons l'exemple error-methodname.js
simple suivant :
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
Ici, nous avons une fonction foo
installée sous le nom "bar"
sur object
. L'exécution de cet extrait de code dans Chromium produit le résultat suivant :
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
Ici, nous voyons la recherche du nom de la méthode en action : le frame de pile le plus élevé appelle la fonction foo
sur une instance de Object
via la méthode nommée bar
. La propriété error.stack
non standard utilise donc beaucoup JSStackFrame::GetMethodName()
. En fait, nos tests de performances indiquent également que nos modifications ont considérablement accéléré les choses.
Pour revenir sur les outils pour les développeurs Chrome, le fait que le nom de la méthode soit calculé même si error.stack
n'est pas utilisé semble incorrect. L'historique de cette méthode nous aide: traditionnellement, V8 avait mis en place deux mécanismes distincts pour collecter et représenter une trace de la pile pour les deux API différentes décrites ci-dessus (l'API v8::StackFrame
C++ et l'API JavaScript de trace de la pile). Avoir deux façons différentes de faire (à peu près) la même chose était source d'erreurs et entraînait souvent des incohérences et des bugs. C'est pourquoi, fin 2018, nous avons lancé un projet visant à définir un seul goulot d'étranglement pour la capture de la trace de la pile.
Ce projet a été un grand succès et a considérablement réduit le nombre de problèmes liés à la collecte de la trace de la pile. La plupart des informations fournies via la propriété error.stack
non standard avaient également été calculées de manière paresseuse et uniquement lorsqu'elles étaient vraiment nécessaires. Toutefois, lors du refactoring, nous avons appliqué le même principe aux objets v8::StackFrame
. Toutes les informations sur le frame de pile sont calculées la première fois qu'une méthode y est appelée.
Cela améliore généralement les performances, mais s'est malheureusement avéré contraire à la façon dont ces objets d'API C++ sont utilisés dans Chromium et DevTools. En particulier, comme nous avions introduit une nouvelle classe v8::internal::StackFrameInfo
, qui contenait toutes les informations sur un bloc de pile exposé via v8::StackFrame
ou error.stack
, nous devions toujours calculer le sur-ensemble des informations fournies par les deux API. Ainsi, pour les utilisations de v8::StackFrame
(et en particulier pour les outils de développement), nous calculions également le nom de la méthode, dès que des informations sur un bloc de pile étaient demandées. Il s'avère que DevTools demande toujours immédiatement des informations sur la source et le script.
Sur la base de cette constatation, nous avons pu réfactoriser et simplifier de manière drastique la représentation des frames de pile, et la rendre encore plus paresseuse. Ainsi, les utilisations dans V8 et Chromium ne paient désormais que le coût de calcul des informations demandées. Cela a permis d'améliorer considérablement les performances des outils de développement et d'autres cas d'utilisation de Chromium, qui ne nécessitent qu'une fraction des informations sur les blocs de pile (essentiellement le nom du script et l'emplacement source sous la forme du décalage de lignes et de colonnes). Cela a également permis d'améliorer les performances.
Noms des fonctions
En supprimant les refactorisations mentionnées ci-dessus, la surcharge liée à la décodage (temps passé dans v8_inspector::V8Debugger::symbolize
) a été réduite à environ 15% du temps d'exécution global, et nous avons pu voir plus clairement où V8 passait du temps lors de la collecte et du décodage des frames de pile à utiliser dans les outils de développement.
Le premier élément qui s'est démarqué était le coût cumulé du calcul du numéro de ligne et de colonne. La partie coûteuse ici consiste à calculer le décalage de caractère dans le script (en fonction du décalage de bytecode que nous obtenons de V8). Il s'a avéré que, en raison de notre refactoring ci-dessus, nous l'avons fait deux fois, une fois lors du calcul du numéro de ligne et une autre fois lors du calcul du numéro de colonne. La mise en cache de la position source sur des instances v8::internal::StackFrameInfo
a permis de résoudre rapidement ce problème et d'éliminer complètement v8::internal::StackFrameInfo::GetColumnNumber
de tous les profils.
Le résultat le plus intéressant pour nous est que v8::StackFrame::GetFunctionName
était étonnamment élevé dans tous les profils que nous avons examinés. En creusant un peu plus, nous avons réalisé qu'il était inutilement coûteux de calculer le nom que nous affichons pour la fonction dans le frame de pile dans DevTools.
- Nous recherchons d'abord la propriété
"displayName"
non standard. Si elle renvoie une propriété de données avec une valeur de chaîne, nous l'utilisons. - sinon, recherchez la propriété
"name"
standard et vérifiez à nouveau si elle renvoie une propriété de données dont la valeur est une chaîne. - et finalement revenir à un nom de débogage interne inféré par l'analyseur V8 et stocké dans le littéral de fonction.
La propriété "displayName"
a été ajoutée comme solution de contournement pour que la propriété "name"
sur les instances Function
soit en lecture seule et non configurable en JavaScript.Cependant, elle n'a jamais été standardisée et n'a pas été largement utilisée, car les outils pour les développeurs du navigateur ont ajouté l'inférence de nom de fonction qui permet d'effectuer le travail nécessaire dans 99,9% des cas. De plus, ES2015 a rendu la propriété "name"
des instances Function
configurable, ce qui élimine complètement le besoin d'une propriété "displayName"
spéciale. Étant donné que la recherche négative pour "displayName"
est assez coûteuse et pas vraiment nécessaire (ES2015 est sorti il y a plus de cinq ans), nous avons décidé de supprimer la prise en charge de la propriété fn.displayName
non standard de V8 (et des outils de développement).
Une fois la recherche négative de "displayName"
supprimée, la moitié du coût de v8::StackFrame::GetFunctionName
a été supprimée. L'autre moitié est utilisée pour rechercher la propriété générique "name"
. Heureusement, nous avions déjà mis en place une logique pour éviter les recherches coûteuses de la propriété "name"
sur les instances Function
(non modifiées), que nous avons introduite dans la version V8 il y a quelque temps pour accélérer Function.prototype.bind()
. Nous avons porté les vérifications nécessaires, ce qui nous permet de passer d'abord la recherche générique coûteuse. Par conséquent, v8::StackFrame::GetFunctionName
n'apparaît plus dans aucun profil que nous avons envisagé.
Conclusion
Grâce aux améliorations ci-dessus, nous avons considérablement réduit les frais généraux de DevTools en termes de traces de pile.
Nous savons qu'il existe encore plusieurs améliorations possibles. Par exemple, les frais généraux liés à l'utilisation de MutationObserver
restent perceptibles, comme indiqué dans chromium:1077657. Pour le moment, nous avons résolu les principaux problèmes et nous reviendrons peut-être à l'avenir pour optimiser davantage les performances de débogage.
Télécharger les canaux de prévisualisation
Envisagez d'utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces canaux de prévisualisation vous donnent accès aux dernières fonctionnalités de DevTools, vous permettent de tester les API de plates-formes Web de pointe et vous aident à détecter les problèmes sur votre site avant vos utilisateurs.
Contacter l'équipe des outils pour les développeurs Chrome
Utilisez les options suivantes pour discuter des nouvelles fonctionnalités, des mises à jour ou de tout autre élément lié aux outils pour les développeurs.
- Envoyez-nous vos commentaires et vos demandes de fonctionnalités sur crbug.com.
- Signalez un problème dans les outils de développement à l'aide de l'icône Plus d'options > Aide > Signaler un problème dans les outils de développement dans les outils de développement.
- Tweetez à l'adresse @ChromeDevTools.
- Laissez des commentaires sur les vidéos YouTube sur les nouveautés des outils pour les développeurs ou sur les vidéos YouTube sur les conseils concernant les outils pour les développeurs.