Mesures de performances pour la recherche d'applications Web incroyablement rapides

Il y a un dicton: "Ce que vous ne pouvez pas mesurer, vous ne pouvez pas l'améliorer." L'auteur de l'article, dont nous publions aujourd'hui la traduction, travaille pour Superhuman . Il dit que cette entreprise développe le client de messagerie le plus rapide au monde. Nous parlerons ici de ce qui est «rapide» et de la façon de créer des outils pour mesurer les performances d'applications Web incroyablement rapides.



Mesure de vitesse d'application


Afin d'améliorer notre développement, nous avons passé beaucoup de temps à mesurer sa vitesse. Et, comme il s'est avéré, les mesures de performance sont des indicateurs qui sont étonnamment difficiles à comprendre et à appliquer.

D'une part, il est difficile de concevoir des métriques qui décrivent avec précision les sensations ressenties par l'utilisateur lors de l'utilisation du système. D'un autre côté, il n'est pas facile de créer des métriques si précises que leur analyse vous permet de prendre des décisions éclairées. Par conséquent, de nombreuses équipes de développement ne peuvent pas faire confiance aux données qu'elles collectent sur la performance de leurs projets.

Même si les développeurs disposent de mesures fiables et précises, leur utilisation n'est pas facile. Comment définir le terme «rapide»? Comment trouver un équilibre entre vitesse et cohérence? Comment apprendre à détecter rapidement la dégradation des performances ou à évaluer l'impact des optimisations sur le système?

Ici, nous voulons partager quelques réflexions concernant le développement d'outils d'analyse des performances des applications Web.

1. Utiliser la bonne «horloge»


JavaScript dispose de deux mécanismes pour récupérer les horodatages: performance.now() et new Date() .

En quoi diffèrent-ils? Les deux différences suivantes sont fondamentales pour nous:

  • La méthode performance.now() est beaucoup plus précise. La précision de la new Date() construction new Date() est de ± 1 ms, tandis que la précision de performance.now() est déjà de ± 100 µs (oui, il s'agit de microsecondes !).
  • Les valeurs renvoyées par la méthode performance.now() augmentent toujours à un taux constant et sont indépendantes de l'heure système. Cette méthode mesure simplement les intervalles de temps sans se concentrer sur l'heure du système. Et à la new Date() heure new Date() système affecte. Si vous réorganisez l'horloge système, cela modifiera également le retour de la new Date () , ce qui ruinera les données de surveillance des performances.

Bien que les «horloges» représentées par la méthode performance.now() soient évidemment beaucoup mieux adaptées à la mesure des intervalles de temps, elles ne sont pas idéales non plus. performance.now() et new Date() souffrent tous deux du même problème, qui se manifeste dans le cas où le système est en veille: les mesures incluent le moment où la machine n'était même pas active.

2. Vérification de l'activité de l'application


Si vous, en mesurant les performances d'une application Web, passez de son onglet à un autre, cela perturbera le processus de collecte de données. Pourquoi? Le fait est que le navigateur restreint les applications situées dans les onglets d'arrière-plan.

Il existe deux situations dans lesquelles les mesures peuvent être déformées. En conséquence, l'application semblera beaucoup plus lente qu'elle ne l'est réellement.

  1. L'ordinateur passe en mode veille.
  2. L'application s'exécute dans l'onglet d'arrière-plan du navigateur.

La survenue de ces deux situations n'est pas rare. Heureusement, nous avons deux options pour les résoudre.

Premièrement, nous pouvons simplement ignorer les métriques déformées, en rejetant les résultats de mesure qui diffèrent trop de certaines valeurs raisonnables. Par exemple, le code qui est appelé lorsqu'un bouton est enfoncé ne peut tout simplement pas être exécuté pendant 15 minutes! C'est peut-être la seule chose dont vous avez besoin pour résoudre les deux problèmes décrits ci-dessus.

Deuxièmement, vous pouvez utiliser la propriété document.hidden et l'événement visibilitéchange . L'événement visibilitychange est déclenché lorsque l'utilisateur passe de l'onglet du navigateur qui vous intéresse à un autre onglet ou revient à l'onglet qui nous intéresse. Il est appelé lorsque la fenêtre du navigateur minimise ou maximise lorsque l'ordinateur commence à fonctionner, quitte le mode veille. En d'autres termes, c'est exactement ce dont nous avons besoin. De plus, tant que l'onglet est en arrière-plan, la propriété document.hidden est true .

Voici un exemple simple illustrant l'utilisation de la propriété document.hidden et de l'événement visibilitychange .

 let lastVisibilityChange = 0 window.addEventListener('visibilitychange', () => {  lastVisibilityChange = performance.now() }) //    ,      , //  ,   ,     if (metric.start < lastVisibilityChange || document.hidden) return 

Comme vous pouvez le voir, nous supprimons certaines données, mais c'est bien. Le fait est que ce sont des données relatives aux périodes du programme où il ne peut pas utiliser pleinement les ressources du système.

Maintenant, nous avons parlé d'indicateurs qui ne nous intéressent pas. Mais il existe de nombreuses situations, les données collectées dans lesquelles sont très intéressantes pour nous. Voyons comment collecter ces données.

3. Recherchez l'indicateur qui vous permet de mieux saisir l'heure à laquelle l'événement a commencé


L'une des caractéristiques les plus controversées de JavaScript est que la boucle d'événements de ce langage est monothread. À un certain moment, un seul morceau de code est capable de s'exécuter, dont l'exécution ne peut pas être interrompue.

Si l'utilisateur appuie sur le bouton lors de l'exécution d'un certain code, le programme ne le saura que lorsque l'exécution de ce code sera terminée. Par exemple, si l'application a passé 1000 ms dans un cycle continu et que l'utilisateur a appuyé sur le bouton Escape 100 ms après le début du cycle, l'événement ne sera pas enregistré pendant 900 ms supplémentaires.

Cela peut gravement déformer les mesures. Si nous avons besoin de précision pour mesurer exactement comment l'utilisateur perçoit travailler avec le programme, alors c'est un énorme problème!

Heureusement, résoudre ce problème n'est pas si difficile. Si nous parlons de l'événement en cours, nous pouvons, au lieu d'utiliser performance.now() (l'heure à laquelle nous avons vu l'événement), utiliser window.event.timeStamp (l'heure à laquelle l'événement a été créé).

L'horodatage de l'événement est défini par le processus du navigateur principal. Étant donné que ce processus ne se bloque pas lorsque la boucle d'événements JS est verrouillée, event.timeStamp nous donne des informations beaucoup plus précieuses sur le moment où l'événement a été réellement déclenché.

Il est à noter que ce mécanisme n'est pas idéal. Ainsi, entre le moment où le bouton physique est enfoncé et le moment où l'événement correspondant arrive dans Chrome, il s'écoule 9 à 15 ms de temps non comptabilisé ( voici un excellent article à partir duquel vous pouvez savoir pourquoi cela se produit).

Cependant, même si nous pouvons mesurer le temps nécessaire à l'événement pour atteindre Chrome, nous ne devons pas inclure ce temps dans nos statistiques. Pourquoi? Le fait est que nous ne pouvons pas introduire de telles optimisations dans le code qui peuvent affecter de manière significative ces retards. Nous ne pouvons en aucun cas les améliorer.

Par conséquent, si nous parlons de trouver l'horodatage pour le début de l'événement, l'indicateur event.timeStamp semble le plus approprié ici.

Quelle est la meilleure estimation de la fin de l'événement?

4. Désactivez le minuteur dans requestAnimationFrame ()


Une autre conséquence découle des fonctionnalités du périphérique de boucle d'événements en JavaScript: du code qui n'est pas lié à votre code peut être exécuté après, mais avant que le navigateur affiche une version mise à jour de la page à l'écran.

Prenons par exemple React. Après avoir exécuté votre code, React met à jour le DOM. Si vous ne mesurez que le temps dans votre code, cela signifie que vous ne mesurerez pas le temps nécessaire pour exécuter le code React.

Afin de mesurer ce temps supplémentaire, nous utilisons requestAnimationFrame() pour désactiver le minuteur. Cela se fait uniquement lorsque le navigateur est prêt à sortir la trame suivante.

 requestAnimationFrame(() => { metric.finish(performance.now()) }) 

Voici le cycle de vie du cadre (le diagramme est tiré de ce merveilleux matériau sur requestAnimationFrame ).


Cycle de vie du cadre

Comme vous pouvez le voir sur cette figure, requestAnimationFrame() est appelée une fois le processeur terminé, juste avant l'affichage du cadre. Si nous désactivons la minuterie ici, cela signifie que nous pouvons être absolument sûrs que tout ce qui a pris le temps de rafraîchir l'écran est inclus dans les données collectées sur l'intervalle de temps.

Jusqu'ici tout va bien, mais maintenant la situation devient plutôt compliquée ...

5. Ignorer le temps requis pour créer une mise en page et sa visualisation.


Le diagramme précédent, montrant le cycle de vie d'une trame, illustre un autre problème que nous avons rencontré. À la fin du cycle de vie du cadre, il y a des blocs de mise en page (formant une mise en page) et Paint (affichant une page). Si vous ne tenez pas compte du temps nécessaire pour terminer ces opérations, le temps mesuré par nous sera inférieur au temps nécessaire pour que certaines données mises à jour apparaissent à l'écran.

Heureusement, requestAnimationFrame a un autre as dans sa manche. Lorsque la fonction passée par requestAnimationFrame appelée, cette fonction reçoit un horodatage indiquant l'heure de début de la formation de la trame actuelle (c'est-à-dire celle située dans la partie tout à gauche de notre diagramme). Cet horodatage est généralement très proche de l'heure de fin de l'image précédente.

Par conséquent, l'inconvénient ci-dessus peut être corrigé en mesurant le temps total écoulé entre le moment de event.timeStamp et le moment où la formation de la trame suivante commence. Notez le requestAnimationFrame imbriqué:

 requestAnimationFrame(() => {  requestAnimationFrame((timestamp) => { metric.finish(timestamp) }) }) 

Bien que ce qui est montré ci-dessus ressemble à une excellente solution au problème, nous avons finalement décidé de ne pas utiliser cette conception. Le fait est que, bien que cette technique permette d'obtenir des données plus fiables, la précision de ces données est réduite. Les cadres dans Chrome sont formés avec une fréquence de 16 ms. Cela signifie que la précision la plus élevée à notre disposition est de ± 16 ms. Et si le navigateur est surchargé et saute des images, la précision sera encore plus faible et cette détérioration sera imprévisible.

Si vous implémentez cette solution, une amélioration sérieuse des performances de votre code, telle que l'accélération d'une tâche précédemment effectuée de 32 ms, jusqu'à 15 ms, peut ne pas affecter les résultats de la mesure des performances.

Sans prendre en compte le temps nécessaire pour créer une mise en page et sa sortie, nous obtenons des métriques beaucoup plus précises (± 100 μs) pour le code qui est sous notre contrôle. Par conséquent, nous pouvons obtenir une expression numérique de toute amélioration apportée à ce code.

Nous avons également exploré une idée similaire:

 requestAnimationFrame(() => {  setTimeout(() => { metric.finish(performance.now()) } }) 

Cela inclura le temps de rendu, mais la précision de l'indicateur ne sera pas limitée à ± 16 ms. Cependant, nous avons décidé de ne pas utiliser cette approche non plus. Si le système rencontre un long événement d'entrée, alors l'appel à quel setTimeout transmis peut être considérablement retardé et exécuté après la mise à jour de l'interface utilisateur.

6. Clarification du «pourcentage d'événements inférieurs à l'objectif»


Nous développons un projet et misons sur la haute performance, en essayant de l'optimiser de deux manières:

  1. La vitesse. Le temps d'exécution de la tâche la plus rapide doit être aussi proche que possible de 0 ms.
  2. Uniformité. Le temps d'exécution de la tâche la plus lente doit être aussi proche que possible du temps d'exécution de la tâche la plus rapide.

Étant donné que ces indicateurs changent avec le temps, ils sont difficiles à visualiser et difficiles à discuter. Est-il possible de créer un système de visualisation de tels indicateurs qui nous inciterait à optimiser à la fois la vitesse et l'uniformité?

Une approche typique consiste à mesurer le 90e centile de retard. Cette approche vous permet de dessiner un graphique linéaire le long de l'axe Y dont le temps en millisecondes est enregistré. Ce graphique vous permet de voir que 90% des événements sont en dessous du graphique linéaire, c'est-à-dire qu'ils s'exécutent plus rapidement que le temps indiqué par le graphique linéaire.

On sait que 100 ms est la frontière entre ce qui est perçu comme "rapide" et "lent".

Mais que découvrirons-nous comment les utilisateurs se sentent du travail si nous savons que le 90e centile de retard est de 103 ms? Pas particulièrement. Quels indicateurs permettront aux utilisateurs d'être utilisables? Il n'y a aucun moyen de le savoir avec certitude.

Mais que faire si nous savons que le 90e centile du retard est de 93 ms? Le sentiment est que 93 est meilleur que 103, mais nous ne pouvons rien dire de plus sur ces indicateurs, ainsi que sur leur signification en termes de perception des utilisateurs du projet. Encore une fois, il n'y a pas de réponse exacte à cette question.

Nous avons trouvé une solution à ce problème. Elle consiste à mesurer le pourcentage d'événements dont le temps d'exécution ne dépasse pas 100 ms. Cette approche présente trois grands avantages:

  • La métrique est orientée utilisateur. Elle peut nous dire quel pourcentage du temps notre application est rapide et quel pourcentage d'utilisateurs la perçoivent comme rapide.
  • Cette métrique nous permet de ramener les mesures à la précision qui a été perdue du fait que nous n'avons pas mesuré le temps nécessaire pour terminer les tâches à la toute fin de la trame (nous en avons parlé dans la section n ° 5). Étant donné que nous avons défini un indicateur cible qui s'inscrit dans plusieurs cadres, les résultats de mesure qui sont proches de cet indicateur se révèlent être inférieurs à lui ou supérieurs.
  • Cette métrique est plus facile à calculer. Il suffit de calculer simplement le nombre d'événements dont le temps d'exécution est inférieur à l'indicateur cible, puis de les diviser par le nombre total d'événements. Les centiles sont beaucoup plus difficiles à compter. Il existe des approximations efficaces, mais pour tout faire correctement, vous devez prendre en compte chaque dimension.

Cette approche n'a qu'un inconvénient: si les indicateurs sont pires que l'objectif, il sera difficile de remarquer leur amélioration.

7. L'utilisation de plusieurs valeurs seuils dans l'analyse des indicateurs


Afin de visualiser le résultat de l'optimisation des performances, nous avons introduit plusieurs valeurs de seuil supplémentaires dans notre système - supérieures à 100 ms et inférieures.

Nous avons regroupé les retards comme ceci:

  • Moins de 50 ms (rapide).
  • 50 à 100 ms (bon).
  • 100 à 1000 ms (lent).
  • Plus de 1000 ms (terriblement lent).

Des résultats «terriblement lents» nous permettent de constater que nous avons beaucoup manqué quelque part. Par conséquent, nous les mettons en surbrillance en rouge vif.

Ce qui tient en 50 ms est très sensible aux changements. Ici, les améliorations de performances sont souvent visibles bien avant d'être visibles dans un groupe correspondant à 100 ms.

Par exemple, le graphique suivant illustre les performances de l'affichage des threads dans Superhuman.


Afficher le fil

Il montre la période de baisse des performances, puis - les résultats des améliorations. Il est difficile d'évaluer la baisse de performance si vous ne regardez que les indicateurs correspondant à 100 ms (les parties supérieures des colonnes bleues). Lorsque l'on regarde les résultats qui correspondent à 50 ms (les parties supérieures des colonnes vertes), les problèmes de performances sont déjà bien plus visibles.

Si nous avions utilisé l'approche traditionnelle pour étudier les mesures de performance, nous n'aurions probablement pas remarqué un problème dont l'effet sur le système est illustré dans la figure précédente. Mais grâce à la façon dont nous prenons les mesures et à la façon dont nous visualisons nos métriques, nous avons pu trouver et résoudre très rapidement un problème.

Résumé


Il s'est avéré qu'il était étonnamment difficile de trouver la bonne approche pour travailler avec des mesures de performance. Nous avons réussi à développer une méthodologie qui nous permet de créer des outils de haute qualité pour mesurer la performance des applications web. À savoir, nous parlons de ce qui suit:

  1. L'heure de début d'un événement est mesurée à l'aide de event.timeStamp .
  2. L'heure de fin de l'événement est mesurée à l'aide de performance.now() dans le rappel passé à requestAnimationFrame() .
  3. Tout ce qui se passe avec l'application lorsqu'elle se trouve dans l'onglet du navigateur inactif est ignoré.
  4. Les données sont agrégées à l'aide d'un indicateur, qui peut être décrit comme «le pourcentage d'événements inférieurs à l'objectif».
  5. Les données sont visualisées avec plusieurs niveaux de valeurs de seuil.

Cette technique vous donne les outils pour créer des mesures fiables et précises. Vous pouvez créer des graphiques qui montrent clairement une baisse des performances, vous pouvez visualiser les résultats des optimisations. Et le plus important - vous avez la possibilité de réaliser des projets rapides encore plus rapidement.

Chers lecteurs! Comment analysez-vous les performances de vos applications Web?


Source: https://habr.com/ru/post/fr470872/


All Articles