Quand une page de back-office met dix secondes à s’ouvrir, le premier réflexe est de regarder l’infrastructure. On augmente les workers, on ajoute du cache, on soupçonne la base.
Parfois c’est juste. Souvent, c’est un chemin de code parfaitement banal qui explose en milliers de requêtes identiques, et aucune quantité de matériel ne rattrapera ça.
Voici un cas mesuré : une fiche commande en back-office qui déclenchait environ 3 360 requêtes SQL identiques, ramenées à 4 par un correctif de quelques dizaines de lignes.
Le symptôme et sa fausse piste
L’ouverture d’une fiche commande dans le back-office était anormalement lente, sur une installation multiboutique hébergée sur une infrastructure orchestrée où la latence réseau vers la base est plus élevée que sur un serveur unique.
Cette dernière précision compte, parce qu’elle explique pourquoi le problème est devenu visible à ce moment là. Le nombre de requêtes n’avait pas changé. C’est leur coût unitaire qui avait augmenté. Trois mille requêtes à 0,1 milliseconde passent inaperçues, les mêmes à 3 millisecondes deviennent dix secondes d’attente.
La lenteur n’était donc pas causée par l’infrastructure, elle était révélée par elle. C’est une nuance qui change complètement l’endroit où l’on cherche.
L’instrumentation
Impossible d’avancer sans mesurer, et compter les requêtes reste la mesure la plus parlante. La méthode utilisée est brutale et efficace : activer le journal général de MySQL, ouvrir la page une fois, couper le journal, puis compter.
SET GLOBAL general_log = 'ON';
-- ouvrir la page, une seule fois
SET GLOBAL general_log = 'OFF';
Le verdict tombe immédiatement : la même requête revient des milliers de fois.
SELECT SUM(quantity) FROM ps_cart_product WHERE id_cart = ...
Un point d’attention qui a son importance : le profileur SQL intégré ne montrait pas le problème, parce qu’il utilise sa propre classe d’accès à la base. Un outil de diagnostic qui modifie le chemin d’exécution qu’il est censé mesurer est un piège classique. Quand deux instruments se contredisent, celui qui observe depuis l’extérieur du processus a généralement raison.
La cause racine
Sur la fiche commande, le sélecteur de transporteur appelle Carrier::getCarriersForOrder(). Cette méthode, pour présenter les transporteurs disponibles avec leurs frais de port, recalcule le total du panier pour chaque transporteur actif.
En multiboutique avec un nombre conséquent de transporteurs, la multiplication devient rapidement spectaculaire. Chaque recalcul redescend jusqu’à la même somme de quantités, et repart en base.
La question devient alors : pourquoi le cache en mémoire de PrestaShop ne fait-il pas son travail ? Il existe pourtant un mécanisme prévu exactement pour ça.
Deux raisons se cumulent.
Un cache local qui se vide. Le cache en mémoire de la requête est purgé au delà d’un millier d’entrées. Sur une page qui en génère plusieurs milliers, il passe son temps à se remplir et à se vider, et ne sert donc jamais.
Des conditions de mise en cache trop restrictives dans le chemin de calcul de prix, qui font que le résultat n’est de toute façon pas réutilisé d’un appel à l’autre.
Le mécanisme de protection existe, il est simplement inopérant dans ce cas de figure précis. C’est ce qui rend le bug durable : personne ne le cherche, puisque tout le monde suppose que le cache fait son office.
Le correctif
Deux voies s’offraient. Corriger le cœur, ce qui suppose de faire accepter un correctif en amont puis d’attendre une version, avec un problème en production entretemps. Ou contourner de façon bornée.
Nous avons contourné, avec trois contraintes que je recommande de reprendre telles quelles pour ce genre d’intervention.
Une surcharge la plus étroite possible. Une seule méthode ciblée, Carrier::getCarriersForOrder, et rien d’autre. Plus la surface est petite, moins la mise à jour suivante fait mal.
Une mémoïsation en lecture seule, limitée à la durée de la requête HTTP. Le résultat de la somme est mémorisé le temps de l’appel et réutilisé, sans jamais être écrit ailleurs ni persisté. Aucun risque de servir une valeur périmée sur une requête ultérieure.
Un périmètre temporel strict. La mémoïsation n’est active que pendant l’exécution de cette méthode. En sortir, c’est prendre le risque qu’un autre chemin de code lise une valeur figée alors qu’il attend une lecture fraîche.
Le principe, réduit à sa forme la plus simple :
// Mémoïsation portée par la requête, active uniquement
// pendant getCarriersForOrder(). Lecture seule, jamais persistée.
private static array $cartQuantityCache = [];
private static function cartQuantity(int $idCart): int
{
if (!isset(self::$cartQuantityCache[$idCart])) {
self::$cartQuantityCache[$idCart] = (int) Db::getInstance()->getValue(
'SELECT SUM(quantity) FROM ' . _DB_PREFIX_ . 'cart_product
WHERE id_cart = ' . $idCart
);
}
return self::$cartQuantityCache[$idCart];
}
Résultat mesuré : environ 3 360 requêtes ramenées à environ 4 sur l’affichage de la page.
Ce que ce cas apprend
Une page lente n’est pas forcément un problème d’infrastructure. Le réflexe d’augmenter les ressources est compréhensible et parfois nécessaire, mais il masque les vraies causes. Ici, doubler la puissance de la base aurait divisé le temps par deux sur un problème qui pouvait être divisé par mille.
Un cache dont on ne vérifie pas l’efficacité n’est pas un cache. Le mécanisme existait, était activé, et ne servait à rien. Personne ne l’avait remarqué parce que personne ne l’avait mesuré. Un cache doit produire un chiffre : nombre d’appels évités, taux de réutilisation. Sans ce chiffre, c’est une intention, pas une optimisation.
Méfiez-vous des instruments qui changent ce qu’ils mesurent. Le profileur ne voyait rien parce qu’il ne passait pas par le même chemin. Face à un écart entre deux outils, préférez celui qui observe depuis l’extérieur.
Une surcharge bien bornée est une réponse acceptable. L’orthodoxie voudrait qu’on ne surcharge jamais rien. En pratique, une surcharge d’une seule méthode, en lecture seule, active sur un périmètre restreint et documentée par un commentaire qui explique pourquoi elle existe, est infiniment préférable à un problème de production qu’on laisse traîner en attendant une version amont. La vraie faute serait de ne pas noter que la dette existe, et de découvrir dans deux ans que le correctif amont est arrivé et que la surcharge est devenue nuisible.


