Symptôme. Après la montée de version d’un serveur d’intégration continue interne de PHP 8.1 vers PHP 8.3, une suite de 640 tests PHPUnit, jusque-là stable autour de 90 secondes, s’est mise à durer près de 280 secondes, sans qu’aucune ligne de code du projet n’ait changé entre les deux exécutions. Trois hypothèses étaient plausibles : un ralentissement propre à l’interpréteur PHP lui-même, une extension chargée par défaut sur la nouvelle image qui ne l’était pas sur l’ancienne, ou une dépendance de test dont le comportement change selon la version de PHP exécutée.
Diagnostic : isoler une variable à la fois
La première erreur à éviter est de changer plusieurs paramètres en même temps en espérant deviner juste. On commence par exécuter un script minimal, sans aucune dépendance du projet, purement arithmétique, sur les deux versions de PHP pour écarter ou confirmer l’hypothèse de l’interpréteur :
php -r '
$debut = microtime(true);
for ($i = 0; $i < 50000000; $i++) { $x = $i * 2; }
echo microtime(true) - $debut, PHP_EOL;
'
// PHP 8.1 : 0.89 s
// PHP 8.3 : 0.81 s (légèrement plus rapide, comme attendu du moteur seul)
L'interpréteur seul est donc écarté : PHP 8.3 est même un peu plus rapide que PHP 8.1 sur du calcul pur, conformément aux gains habituels annoncés d'une version à l'autre.
Deuxième hypothèse : une extension chargée par défaut
On compare la liste des extensions actives sur les deux environnements :
php -m > extensions-8.1.txt
# sur le nouveau serveur
php -m > extensions-8.3.txt
diff extensions-8.1.txt extensions-8.3.txt
Le diff révèle que Xdebug, absent de l'image PHP 8.1 précédente, est chargé par défaut sur la nouvelle image PHP 8.3, y compris en mode develop qui ralentit sensiblement l'exécution même sans collecte de couverture activée. C'est une cause fréquente et facile à corriger :
php -v
# PHP 8.3.9 (cli) (built: ...)
# with Xdebug v3.3.1, Copyright (c) 2002-2024, by Derick Rethans
# désactiver Xdebug pour l'exécution des tests quand la couverture n'est pas nécessaire
php -d xdebug.mode=off vendor/bin/phpunit

Cette seule correction a ramené la durée de 280 à environ 165 secondes, un progrès net mais insuffisant pour retrouver les 90 secondes d'origine : une troisième cause restait donc à identifier.
Troisième hypothèse : une dépendance de test dont le comportement a changé
Le profilage avec Xdebug lui-même, cette fois activé volontairement en mode profile sur un sous-ensemble réduit de la suite, a mis en évidence qu'une bibliothèque tierce de génération de données factices (fzaninotto/faker, alors non maintenue) instanciait un nouveau générateur aléatoire à chaque appel plutôt qu'une seule fois, un comportement resté inaperçu en PHP 8.1 mais dont le coût s'est révélé nettement plus élevé sous PHP 8.3 à cause d'un changement interne dans la génération de nombres aléatoires cryptographiquement sûrs utilisée par cette bibliothèque.
protected function setUp(): void {
parent::setUp();
$this->faker = \Faker\Factory::create( 'fr_FR' ); // recréé à chaque test, coûteux
}
Le correctif a consisté à instancier ce générateur une seule fois pour l'ensemble de la suite, via une propriété statique partagée entre les tests, et à migrer vers un remplacement activement maintenu :
private static ?\Faker\Generator $faker = null;
protected function setUp(): void {
parent::setUp();
if ( self::$faker === null ) {
self::$faker = \Faker\Factory::create( 'fr_FR' );
}
}
Prévention pour la prochaine montée de version
- Comparer systématiquement la liste des extensions actives avant et après toute montée de version d'infrastructure, pas seulement la version de PHP annoncée.
- Désactiver explicitement Xdebug en dehors des exécutions qui en ont réellement besoin, via une variable d'environnement dédiée plutôt qu'une configuration implicite de l'image.
- Garder un œil sur les dépendances de test elles-mêmes lors d'une montée de version, elles sont aussi susceptibles de changer de comportement que le code applicatif.
En résumé
Ce billet ne traite pas du JIT de PHP, une piste que nous avons envisagée en premier lieu par réflexe mais qui s'est révélée hors sujet dans ce cas précis, le JIT n'étant de toute façon pas activé par défaut sur les images CLI standards utilisées ici. Le ralentissement provenait d'une combinaison de deux causes indépendantes, une configuration d'image et une dépendance de test mal utilisée, ce qui rappelle qu'un diagnostic de performance gagne toujours à isoler chaque variable plutôt qu'à incriminer la cause la plus visible en premier.