Bluebird vs Native vs Async/Await - 2020 État des performances des promesses

Lorsque nous avons commencé les travaux sur la version 2 de Kuzzle il y a un an, la plus grande question était de savoir ce que nous allions faire pour les promises actuelles utilisées dans le coeur de Kuzzle.

Read in english 

 

Aujourd’hui nous avons mis à jour les benchmarks 2020 avec les performances de la dernière version du runtime Javascript basé sur v8: Node.js 14.

 

TLDR;

Dans les dernières versions de Node.js (12 et 14):

  • Utilisez async/await pour l’exécution séquentielle de promises
  • Utilisez Bluebird pour l’exécution parallèle de promises

En général:

  • Bluebird conserve la plus faible empreinte mémoire dans tous les cas
  • Les performances des promises étaient globalement meilleures en Node.js 10

Doxbee benchmark

Pour l’ensemble de nos tests, nous avons utilisés les benchmark de Doxbee développés par l’équipe de Bluebird en s’appuyant sur un article d’analyse des différents patterns asynchrones en Javascript.

 

Ces benchmarks simulent une situation dans laquelle des requêtes sont exécutées séquentiellement ou parallèlement avec des actions bloquantes de lecture/écriture.

 

Pour chaque version de node, nous lanceront les benchmark séquentiels puis parallèles avec les commandes suivantes:

 

$ echo ./doxbee-sequential/promises-native-async-await.js ./doxbee-sequential/promises-ecmascript6-native.js ./doxbee-sequential/promises-bluebird.js | sed -e 's|\.js||' | xargs node ./performance.js --p 1 --t 1 --n 100000

$ echo ./madeup-parallel/promises-ecmascript6-native.js ./madeup-parallel/promises-bluebird.js ./madeup-parallel/promises-native-async-await.js| sed -e 's|\.js||' | xargs node ./performance.js --p 25 --t 1 --n 30000

 

Pour les reproduire, vous pouvez cloner ce dépôt Github: https://github.com/petkaantonov/bluebird/

Les outils de benchmarking sont dans le dossier benchmark/.

Versions de Node.js testées

Pour ce benchmark, nous allons uniquement comparer les versions LTS de Node.js (c-a-d les numéro pairs).

Node.js 6.17.1

Cette ancienne version de Node.js était réputée pour la faible performance de ses promises natives.

Les promises gérées avec async/await n’étaient pas encore disponibles.

Node.js 8.17.0

Cette version est la première à profiter de la nouvelle gestion des promises avec async/await.

Node.js 10.20.1

Cette version inclut de nombreuses amélioration des performances des promises.

Pour la première fois, les promises natives atteignent des performances comparables à Bluebird.

Node.js 12.16.3

Cette version inclut un important gain de performance des promises gérées avec async/await par rapport aux promises standard. (Voir le blog de v8: https://v8.dev/blog/fast-async)

On note cependant une baisse général des performances des promises par rapport à Node.js 10.

Node.js 14.2.0

On constate une amélioration des performances des promises natives dans la dernière version de Node.js.

 

Performance des promises séquentiels

 

 

On constate que depuis Node.js 12, l’exécution séquentiel de promises est plus rapide avec async/await.

 

Cela nous arrange bien car l’utilisation de async/await pour gérer les promises est justement pensée pour les enchaînements séquentiel afin de se rapprocher de la programmation synchrone.

 

const user = await fetch('/api/users/1');

const job = await fetch(`/api/jobs/${user. jobId}`);

const colleagues = await fetch(`/api/users/byJob/${job.id}`);

 

On note également que Bluebird conserve toujours la plus faible empreinte mémoire.

 

Performance des promises parallèles

 

 

Pour l’exécution parallèle des promises, on constate que Bluebird a des performances jusqu’à 4x plus élevées que les promises natives de Node.js.

 

Cela peut s’expliquer avec les nombreux efforts de performances réalisés par le créateur de la librairie: https://www.reaktor.com/blog/javascript-performance-fundamentals-make-bluebird-fast/

 

const userIds = [21, 42, 84, 168];

const promises = userIds.map(id => fetch(`/api/users/${id}`));

const users = await Bluebird.all(promises);

 

On préfèrera donc l’utilisation de Bluebird pour toute manipulation parallèle de promises.

 

De plus la librairie possède de nombreuses méthodes de haut niveau pour faciliter la gestion de traitements parallèles comme la méthode Bluebird.map qui permet de limiter le nombre de promises executées en parallèle:

 

const users = await Bluebird.map(
  userIds,
  id => fetch(`/api/users/${id}`),
  { concurrency: 10 }
);

 

Mais que c’est-il passé après Node.js 10 ?

Si on reprend les deux graphiques sur les performances séquentielles et parallèles, on s’aperçoit que la sortie de Node.js 10 a apporté des améliorations significatives des performances mais ensuite à partir de Node.js 12 ces performances ont drastiquement diminué.

 

 

 

 

En faisant plus d’essais, on constate que ce changement intervient entre les version 7.4 et 7.5 de v8, le moteur Javascript de Node.js et Chrome.

 

Je n’ai rien trouvé de spécial à ce propos dans les releases notes. Si vous pensez avoir la réponse vous pouvez participer sur le bugtracker de v8.

 

EDIT: 10/07/2020


Dans la première version de cet article, j'ai remarqué d'étranges résultats: Node.js 10 avait les meilleures performances.

 

Il s'avère que c'est à cause de la taille des pages de mémoire qui a doublé dans v8 entre Node.js 8 et Node.js 10.

L'outil de benchmark devait itérer sur chaque page pour calculer la mémoire utilisée, donc il était au moins deux fois plus lent dans les dernières versions de Node.js.

 

Merci à Camillo Bruni d'avoir trouvé la source du problème!

 

Les benchmarks de performance ont été mis à jour en désactivant le calcul de la mémoire.

 

Plus d'informations ici.

 

Sources des graphiques

Adrien Maret

Postes associés