Le problème de la mesure de l'exécution de votre application Node.js

Mar 07 2023
Mesurer le temps d'exécution d'une application Node.js est un aspect essentiel de l'optimisation des performances.
Photo par Chris Gonzalez de Burst Copié

Mesurer le temps d'exécution d'une application Node.js est un aspect essentiel de l'optimisation des performances. En mesurant la durée d'exécution de différentes parties de votre application, vous pouvez identifier les goulots d'étranglement et optimiser votre code pour améliorer les performances.

Comment mesurer le temps d'exécution d'une fonction ?

Il existe de nombreuses façons de mesurer le temps dans node.js, la façon la plus haute résolution de le faire est d'utiliser process.hrtime() ou Perf_hooks , les deux sont meilleurs que d'utiliser la classe Date car ils ne s'appuient pas sur votre horloge système, mais encore, il y a peu de différences entre eux:

process.hrtime() fournit une résolution de niveau nanoseconde, tandis que perf_hooks fournit une résolution de niveau microseconde. Cela signifie que process.hrtime() est généralement plus précis mais aussi plus coûteux à appeler.

Prenons maintenant un exemple de mesure du temps d'exécution de la logique suivante. Nous avons une logique asynchrone (nous pouvons supposer qu'il s'agit d'un appel HTTP) qui se résout après 1000 ms. Nous utiliserons perf_hooks pour mesurer le temps qu'il faut pour tenir cette promesse.

const {performance} = require('perf_hooks');
const SOME_BIG_NUMBER = 4000000000;

async function asyncLogic(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const start = performance.now();
asyncLogic(1000).then(() => {
    const time = performance.now() - start;
    console.log(`Time taken: ${time}ms`);
});

let x = 0;
for (let i = 0; i < SOME_BIG_NUMBER; i++) {
    x += Math.sqrt(SOME_BIG_NUMBER);
}

Time taken: 3948ms

La boucle d'événements et la file d'attente d'événements

Le modèle d'exécution JavaScript est basé sur la boucle d'événement, c'est la partie qui est responsable de l'exécution de notre code et nous permet d'utiliser une logique asynchrone.

Lorsque nous exécutons du code JavaScript, voici ce qui se passe :

  1. JavaScript exécute tout le code synchrone dans l'ordre dans lequel il apparaît dans le programme.
  2. S'il y a du code asynchrone (comme notre fonction asyncLogic), JavaScript l'ajoutera à la file d'attente des événements au lieu de l'exécuter immédiatement.
  3. La boucle d'événements vérifie constamment la file d'attente d'événements pour voir s'il y a des événements en attente d'exécution.
  4. Lorsqu'un événement est prêt à être exécuté, la boucle d'événements le déplace de la file d'attente d'événements vers la pile d'appels , où il peut être exécuté.
  5. Une fois l'événement exécuté, tout code synchrone supplémentaire est exécuté avant que la boucle d'événements ne revienne vérifier la file d'attente d'événements pour plus d'événements.

C'est pourquoi dans notre exemple, la mesure du temps était de 4000ms,

  1. Nous exécutons la fonction asyncLogic, qui a été ajoutée à la file d'attente des événements.
  2. Ensuite, nous exécutons notre logique synchrone, qui nous prend environ 4000 ms pour s'exécuter.
  3. La boucle d'événements déplace notre rappel asyncLogic() de la file d'attente d'événements vers la pile d'appels
  4. Nous exécutons ensuite la fonction de rappel et calculons l'heure de fin

Décalage de boucle d'événement

Par conséquent, lors de la surveillance des performances de notre application, il y a une métrique importante à laquelle nous devons prêter attention, le décalage de la boucle d'événements, le temps pris par la boucle d'événements entre l'ajout d'un nouveau rappel à la file d'attente d'événements jusqu'à l'exécution de ce rappel.

Nous pouvons facilement créer une boucle qui nous permettra de mesurer notre décalage de boucle d'événement avec le code suivant :

function measureEventLoopLag() {
    let lastLoopTime = performance.now();
    setTimeout(() => {
        const delay = performance.now() - lastLoopTime - 1000;
        lastLoopTime = performance.now();
        console.log(`Event Loop lag is: ${delay} ms`)
        measureEventLoopLag();
    }, 1000)
}

Amélioration de l'exécution

Supposons que notre code a un appel HTTP qui nous prend 4000 ms, et nous avons également une boucle lente qui nous prend également environ 4000 ms pour s'exécuter. Comme nous l'avons vu dans notre exemple précédent, lorsque nous mesurons ce temps d'exécution, nous découvrirons qu'il nous a fallu 4000 ms pour exécuter tout le code.

Après avoir refactorisé notre code, nous réduisons le temps d'exécution de la boucle lente de 50 %. Lorsque nous avons réexécuté le code, le temps d'exécution global selon notre mesure n'a pas vraiment changé, nous avons encore attendu environ 4000 ms pour terminer le flux global car nous avons attendu que notre flux asynchrone soit terminé. On dirait que notre refactor n'a pas vraiment aidé.

Pas vraiment, si nous regardons en termes de CPU, nous avons fait une énorme différence. Nos appels HTTP ne consomment pas beaucoup de CPU, nous attendons la plupart du temps la réponse. Donc la plupart du temps, notre CPU exécutait notre boucle lente, quand nous avons refactorisé le code et l'avons réduit de 50% cela n'a pas changé le temps global mais cela a eu un effet significatif sur notre CPU.

Conclusion

Dans notre exemple, nous avions une très petite logique à surveiller, les choses se compliquent vraiment lorsque vous avez un système complexe avec beaucoup de logique à exécuter, dans ce cas vous devriez essayer de mesurer de petites parties de votre logique pour mieux surveiller la baisse des performances.

Il y a deux choses importantes à noter lors de la surveillance de l'exécution de votre application :

  • La mesure du temps de fonction asynchrone peut être affectée par le code synchrone (retard de boucle d'événement)
  • Les fonctions de mesure qui ont à la fois une logique asynchrone et synchrone peuvent masquer l'amélioration (ou le déclin) de la durée d'exécution.
  • Un décalage de boucle d'événement supérieur à ~ 30 ms est vraiment mauvais pour votre application.