Tester les performances avec phpbench

Publié le 24 janvier 2024 par Mathieu De Gracia
Couverture de l'article Tester les performances avec phpbench

Vous êtes probablement familier avec l'utilisation de solutions telles que PHPUnit ou Pest pour tester la conformité et le comportement de votre code, mais qu'en est-il de ses performances ?

Un test unitaire ou de feature se concentre essentiellement sur la validation d'un cas d'utilisation, sur l'absence de régression, laissant de coté la rapidité d'exécution de votre code.

Découvrons aujourd'hui le package phpbench/phpbench nous permettant d'observer et de surveiller la quantité de mémoire consommée ainsi que le temps nécessaire à l'exécution du code !

  1. Installation
  2. Mon premier test
  3. Améliorer la fiabilité des résultats
  4. Les assertions
  5. Précautions d'usage
  6. Utiliser les tests de bench dans Laravel

Installation

Le package nécessite au minimum une version 8.1 de PHP :

1composer require phpbench/phpbench --dev

Une fois téléchargé, il sera nécessaire de créer un fichier de configuration à la racine de votre projet dénommé phpbench.json avec la configuration suivante :

1{
2 "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json",
3 "runner.bootstrap": "vendor/autoload.php"
4}

Le package est désormais prêt à exécuter vos premiers tests de performance !

1vendor/bin/phpbench

Mon premier test

Dans le cadre de ce tutoriel, nous allons créer une classe ComputeService contenant une méthode iterate et possédant une simple boucle de 10'000 itérations, cette classe sera par la suite le cobaye de ce tutoriel :

1<?php
2 
3namespace App\Services;
4 
5class ComputeService
6{
7 public function iterate(): void
8 {
9 for ($i=0; $i < 10_000; $i++) {
10 //
11 }
12 }
13}

Ajoutons maintenant notre premier test de performance, communément appelé un bench, de notre classe ComputeService :

1<?php
2 
3namespace Tests\Benchmark;
4 
5use App\Services\ComputeService;
6 
7class ComputeServiceBench
8{
9 public function benchIterate(): void
10 {
11 $computeService = new ComputeService();
12 
13 $computeService->iterate();
14 }
15}

Par convention, il est nécessaire que la méthode de test soit préfixée du mot "bench", nous vous conseillons également de suffixer le nom de la classe de bench afin d'éviter des conflits de nommage.

Notre premier test de performance est désormais fonctionnel, comme vous pouvez le voir, un test de bench se résume à exécuter une section de code, dans notre cas la méthode iterate, et d'y mesurer le temps d'exécution et la consommation en mémoire nécessaire.

Nous pouvons désormais lancer notre première analyse avec la commande suivante :

1vendor/bin/phpbench run tests/Benchmark --report=aggregate
1$ vendor/bin/phpbench run tests/Benchmark --report=aggregate
2PHPBench (1.2.15) running benchmarks... #standwithukraine
3with configuration file: laravel-10/phpbench.json
4with PHP version 8.1.24, xdebug ✔, opcache ✔
5
6\Tests\Benchmark\ComputeServiceBench
7
8 benchIterate............................I0 - Mo360.000μs (±0.00%)
9
10Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
11+---------------------+--------------+-----+------+-----+----------+-----------+--------+
12| benchmark | subject | set | revs | its | mem_peak | mode | rstdev |
13+---------------------+--------------+-----+------+-----+----------+-----------+--------+
14| ComputeServiceBench | benchIterate | | 1 | 1 | 6.743mb | 360.000μs | ±0.00% |
15+---------------------+--------------+-----+------+-----+----------+-----------+--------+

Le résultat d'une analyse peut sembler intimidant au premier abord mais les 3 informations essentielles sont :

Le mem_peak, pour pic de mémoire, correspond à la consommation en mémoire maximum (en mb) de votre code.

Le mode, plus précisément le Kernel Density Estimation, est une estimation statistique de la valeur la plus probable dans un ensemble de données, cela n'est pas une moyenne. Dans notre contexte, le mode représente le temps d'exécution le plus fréquemment observé lors de l'exécution du code.

Pour finir, le rstdev, pour coefficient de variation, est également une mesure statistique indiquant à quel point les valeurs d'un ensemble de données sont dispersées par rapport à leur moyenne. Une valeur élevée de rstdev suggérera que notre code n'a pas une consommation en ressources constante d'une exécution à l'autre.

Améliorer la fiabilité des résultats

Notre précédente analyse était basée sur une seule et unique exécution du code ... le résultat obtenu était bien trop dépendant des aléas temporaire de la machine et potentiellement non représentatif des performances de notre classe.

Afin d'obtenir des résultats statistiquement plus pertinents, il est possible d'exécuter des lots comprenant plusieurs itérations à l'aide des annotations suivantes :

1<?php
2 
3namespace Tests\Benchmark;
4 
5use App\Services\ComputeService;
6 
7class ComputeServiceBench
8{
9 /**
10 * @Revs(1000)
11 * @Iterations(3)
12 */
13 public function benchIterate(): void
14 {
15 $computeService = new ComputeService();
16 
17 $computeService->iterate();
18 }
19}

Des options de commande sont aussi possibles : --iterations=3 --revs=1000

Dans cette configuration, le test effectuera trois lots de 1'000 exécutions chacun.

En réalisant ces multiples analyses nous réduirons les risques d'obtenir des faux négatifs tout en augmentant le volume de données accumulées nous permettant ainsi d'obtenir des moyennes et des résultats bien plus significatifs.

Les assertions

Maintenant que notre test fonctionne, il est temps de s'assurer que le code répond à des contraintes de performance plus strict.

Pour ce faire, PHPBench propose tout un ensemble d'assertions permettant de valider certaines contraintes lors de l'exécution du code.

Par exemple, nous pouvons vérifier que l'exécution du code s'effectue dans un intervalle de temps spécifique et en consommant une certaine quantité de mémoire à l'aide des annotations suivantes :

1/**
2 * @Revs(1000)
3 * @Iterations(3)
4 * @Assert("mode(variant.time.avg) < 50 microseconds +/- 10%")
5 * @Assert("mode(variant.mem.peak) < 6 megabytes +/- 10%")
6 */

Grace à ce test, nous nous protégerons des possibles régressions de la méthode iterate si sa consommation en mémoire ou son temps d'exécution dépasse les limites autorisés.

Les unités de temps et de mémoire possibles sont disponibles dans la documentation.

Précautions d'usage

PHPBench est destiné à tester du code dont le comportement est à la fois déterministe et, surtout, indépendant de services tiers.

Par exemple, une méthode faisant appel à une base de données verra la pertinence de ses résultats fortement dégradée, voire imprévisible, car la base de données est un tiers dont les aléas techniques peuvent se répercuter de manière aléatoire sur votre code.

Dans une chaîne de dépendance, le maillon le plus lent dicte toujours le rythme.

Dans cette situation, il sera nécessaire de mock les interactions avec ces services tiers si vous souhaitez tout de même utiliser PHPBench et obtenir des résultats fiables.

Considérant ces quelques contraintes, les tests de performances que vous pourrez écrire avec PHPBench se rapprocheront d'avantage de tests unitaires que de tests de features.

Pour une raison similaire, il sera important de veiller à désactiver les extensions de PHP pouvant consommer de manière aléatoire de la ressource, comme par exemple la présence d'un debugger comme Xdebug.

Pour ce faire, le plus simple sera de désactiver toute la configuration par défaut de PHP, dont les extensions, depuis le fichier précédemment créé phpbench.json à l'aide de la clé runner.php_disable_ini :

1{
2 "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json",
3 "runner.bootstrap": "vendor/autoload.php",
4 "runner.php_disable_ini": true
5}

Ne négligez pas cette contrainte, concernant ce tutoriel, la désactivation de Xdebug a permis un gain de mem_peak de 19% et de rapidité d'exécution du mode de plus de 75%.

Désactiver les extensions PHP aura également permis d'améliorer la note de rstdev et donc d'obtenir des résultats plus fiables et moins aléatoires.

Vous retrouverez l'ensemble de la configuration disponible à cette URL de la documentation.

Utiliser les tests de bench dans Laravel

Le package PHPBench ne dispose pas de testcase lui permettant de générer une instance de Laravel, cela signifie que le framework ne sera pas accessible dans vos tests de performances, que ce soit au niveau des méthodes de bench ou même des classes que vous manipulerez !

Regardons les exemples suivants pour bien comprendre les contraintes que cela provoque. L'utilisation du helper config, propre à Laravel, entraînera une fatal error si vous l'utilisez dans la méthode benchIterate :

1public function benchIterate(): void
2{
3 echo config('app.name'); // 💥 Class "config" does not exist
4 
5 $computeService = new ComputeService();
6 
7 $computeService->iterate();
8}

Plus problématique encore, cette contrainte sera également présente dans la méthode iterate de notre ComputeService, dans le contexte d'utilisation de PHPBench, Laravel n'est tout simplement pas instancié en amont :

1class ComputeService
2{
3 public function iterate(): void
4 {
5 echo config('app.name'); // 💥 Class "config" does not exist
6 
7 for ($i=0; $i < 10_000; $i++) {
8 //
9 }
10 }
11}

Pour pallier à cette problématique, il sera envisageable de réutiliser le trait CreatesApplication, initialement présent dans nos tests de features, afin de rendre Laravel disponible dans nos benchs :

1<?php
2 
3namespace Tests\Benchmark;
4 
5use Tests\CreatesApplication;
6use App\Services\ComputeService;
7 
8class ComputeServiceBench
9{
10 use CreatesApplication;
11 
12 /**
13 * @Revs(1000)
14 * @Iterations(3)
15 * @Assert("mode(variant.time.avg) < 50 microseconds +/- 10%")
16 * @Assert("mode(variant.mem.peak) < 6 megabytes +/- 10%")
17 * @BeforeMethods({"setUp"})
18 */
19 public function benchIterate(): void
20 {
21 $computeService = new ComputeService();
22 
23 $computeService->iterate();
24 }
25 
26 public function setUp(): void
27 {
28 $this->createApplication();
29 }
30}

Attention toutefois, initialisé Laravel consommera de la ressource et pourrait se répercuter négativement sur la valeur de rstdev et la pertinence des résultats obtenus par vos tests de performance.

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie