Une fonction de calcul de prix dégressif, appelée à chaque affichage de fiche produit sur un site e-commerce, est passée de 3 millisecondes à 210 millisecondes après l’ajout d’une nouvelle règle de remise imbriquée dans une boucle mal placée. Aucun test fonctionnel n’a détecté le problème : la fonction retournait toujours le bon prix, juste beaucoup plus lentement. C’est un client final qui a signalé la lenteur du site, trois semaines après la mise en ligne du correctif fautif.
Un test fonctionnel vérifie un résultat, jamais un temps. Pour attraper ce genre de régression avant qu’elle n’atteigne la production, il faut un test différent, dont la seule assertion porte sur la durée d’exécution comparée à une référence connue.
Architecture du dispositif de benchmark continu
projet/
├── src/
│ └── Pricing/
│ └── DegressiveCalculator.php
├── benchmarks/
│ ├── DegressiveCalculatorBench.php
│ └── baseline.json (ligne de base versionnée dans le dépôt)
└── .github/
└── workflows/
└── benchmark.yml
Écrire le benchmark avec PHPBench

PHPBench est l’outil de référence pour ce type de mesure en PHP, avec un fonctionnement proche de PHPUnit mais orienté vers la mesure plutôt que l’assertion de résultat :
/**
* @BeforeMethods({"preparerContexte"})
* @Revs(1000)
* @Iterations(5)
*/
class DegressiveCalculatorBench {
private $produits;
public function preparerContexte() {
$this->produits = array_fill( 0, 50, [ 'prix' => 29.90, 'quantite' => 12 ] );
}
public function bench_calcul_prix_degressif() {
$calculateur = new DegressiveCalculator();
foreach ( $this->produits as $produit ) {
$calculateur->calculer( $produit['prix'], $produit['quantite'] );
}
}
}
L’annotation @Revs(1000) répète l’appel mille fois par itération pour lisser le bruit de mesure, et @Iterations(5) répète l’ensemble cinq fois pour calculer un écart-type fiable.
Comparer contre une ligne de base versionnée
PHPBench sait comparer une exécution à un fichier de référence généré préalablement et commité dans le dépôt :
# générer la ligne de base une première fois, après validation manuelle des temps
vendor/bin/phpbench run --store --tag=baseline
# en CI, comparer contre cette référence
vendor/bin/phpbench run --ref=baseline --report=aggregate
PHPBench renvoie un code de sortie différent de zéro si un seuil de régression configuré est dépassé, ce qui suffit à faire échouer le job GitHub Actions correspondant.
Définir un seuil de tolérance réaliste
Un seuil à 0 % de tolérance ferait échouer le pipeline à la moindre variation de charge du runner CI partagé, un bruit qui n’a rien à voir avec une vraie régression du code. Après plusieurs semaines d’observation sur nos runners GitHub Actions standards, nous avons fixé la tolérance à 40 % d’écart avant échec, un seuil volontairement large qui absorbe le bruit machine tout en attrapant sans difficulté une régression d’un facteur 10 comme celle de l’incident initial.
# .phpbench.json
{
"runner.assert": "mode(current.time) < mode(baseline.time) * 1.4"
}
Intégrer au pipeline GitHub Actions
- name: Benchmark de performance
run: vendor/bin/phpbench run --ref=baseline --report=aggregate --fail-on-regression
- Ne benchmarker que les fonctions identifiées comme critiques par leur fréquence d'appel, pas l'intégralité du code : le coût de maintenance croît vite avec le nombre de benchmarks.
- Regénérer la ligne de base explicitement et volontairement après une optimisation reconnue, jamais automatiquement.
- Exécuter les benchmarks sur un runner dédié si possible, pour limiter le bruit de mesure lié au partage de ressources.
Pour aller plus loin
Ce dispositif ne remplace pas Lighthouse CI, qui mesure la performance perçue d'une page entière côté navigateur : il se concentre sur la performance d'une fonction PHP isolée, à un grain beaucoup plus fin. Les deux approches sont complémentaires plutôt que redondantes, la première protégeant l'expérience utilisateur globale, la seconde protégeant les fonctions critiques identifiées comme sensibles par l'équipe.