Optimiser son code grâce à Blackfire.io

blackfire-get-started

Depuis plusieurs mois chez CCM Benchmark, nous utilisons régulièrement blackfire.io pour avoir une vision simple des optimisations possibles dans un code, y compris en production.

Blackfire.io : un autre outil d’APM ?

Non, blackfire.io est extrêmement complémentaire des outils d’Application Performance Management que l’on rencontre au quotidien (par exemple NewRelic et tous ses petits collègues). Il s’agit en effet d’un profiler. Cela signifie qu’il est déclenché à la demande pour comprendre ce qui se passe dans une application. Il se rapproche à ce sens davantage des outils type XDebug ou XHProf, avec plusieurs atouts en poche.

1 – Le profiling en prod

Contrairement à XDebug, Blackfire.io a été conçu de manière à pouvoir être installé en production avec aucun overhead en dehors des requêtes spécifiques de profiling. Blackfire prend la forme de 2 outils : une extension PHP qui effectue la collecte des données et un agent qui se charge de transmettre les données à la plateforme saas. Lorsqu’une requête ne contiens pas les headers spécifiques de profiling, l’extension est tout simplement désactivée et ne collecte aucune donnée. A contrario, lorsque le profiling est activé, l’extension prend le relais et ajoute des points d’écoute dans le code afin de pouvoir tracer tous les appels. L’activation du profiling & la visualisation des métriques se fait grâce à une extension pour Chrome très agréable à utiliser.

2 – L’agrégation des requêtes

Il arrive bien souvent que lorsqu’on utilise xdebug ou xhprof et qu’on génère plusieurs fois de suite des call grinds, elles soient totalement dissemblables les unes des autres. Blackfire.io offre l’avantage pour les hits webs d’agréger 10 requêtes ensemble afin de lisser les différences. Cela signifie que si on relance une inspection quelques minutes plus tard, les call grind seront très semblables. Cela permet de travailler beaucoup plus facilement avec les graps générés.

3 – La suppression du bruit

Il s’agit ici d’un point très intéressant. Qui a envie de savoir que dans toute la page, la méthode « strpos » a été appelée 1052 fois ? Personne, sauf si ces appels commencent à être coûteux. Afin d’avoir des call grind lisibles, blackfire.io regroupe tous les appels de fonctions standards de PHP qui ne consomment peu de CPU et peu de mémoire. Par contre si certaines fonctions commencent à peser, alors elles apparaîtront dans la stack. Pratique pour retrouver ses petits.

4 – Le profiling en prod… Mais c’est pas un peu tard ?

Effectivement, profiler sa prod c’est un peu tard dans un workflow. C’est une étape nécessaire pour avoir une application performante, mais ça ne doit surtout pas être la seule ! C’est pour cette raison que blackfire fonctionne aussi bien dans tous les environnements qu’en production. Cela permet de s’assurer au plus tôt dans le développement d’une feature, que celle-ci est suffisamment optimisée.

5 – Ok je profile en dév, mais j’en ai un peu marre de le faire à la main…

Je comprends, vérifier simplement qu’une page répond avec les standards imposés pour le projet est une tâche répétitive et sans valeur ajoutée (au contraire de l’optimisation qui peut en découler). Comme toutes les tâches sans valeur ajoutée, celle-ci doit être évacuée vers un outil permettant d’automatiser la vérification de ces métriques. Ca tombe plutôt bien car récemment blackfire permet de faire ça (déclencher des vérifications de performance sur Builds de CI, intégrer dans ses tests unitaires des assertions de performance sur certaines méthodes ou dans des tests fonctionnels, etc.). En gros blackfire nous aide à automatiser la gestion de la performance sur une application à toutes les étapes. Si c’est pas beau ça !

6 – Le truc qui manque

Parce qu’il en faut quand même un, je note un point qui manque actuellement à Blackfire. Ca semble être quelque part dans la roadmap de l’équipe (sûrement un peu bas dans la pile) : j’aimerais pouvoir faire en sorte que la toolbar qui apparaît après un profiling puisse me suivre lorsque je me ballade en prod ou en recette : en gros je ne déclenche plus manuellement mes profiling mais je peux avoir (sur une seule requête du coup) un aperçu rapide de ce qui se passe. Et si je veux aller voir plus en détails, je déclenche un profiling complet. Ca serait pas beau ça ?

Et si on testait blackfire.io ?

Afin de pouvoir montrer comment fonctionne concrètement blackfire, j’ai trouvé 2 cas d’optimisation que j’ai pu effectuer grâce à lui (et qui aurait été compliquées à réaliser sans un outil de ce type).

1 – Ting : l’optimisation simple mais efficace

En vérifiant la performance d’une de nos applications en production, qui utilise Symfony 2 ainsi que Ting, je tombe sur cette call stack :

findMetadata

 

On voit ici que la 2ème méthode a consommer le plus de temps est issue de Ting, et permet d’associer une table de la base de données à ses Métadonnées. En regardant le code, voici ce qui se trouve à cet endroit :

Capture d'écran de 2015-12-01 10:48:22

 

Et on tombe dans un cas qui peut sembler inoffensif: un simple foreach sur une liste relativement courte. Oui, mais ce code peut être appelé de nombreuses fois dans une même page. Un simple ajout d’un tableau de pseudo-cache et le tour est joué !

Capture d'écran de 2015-12-01 10:52:45

Un petit comparatif avant / après sur blackfire va nous permettre de voir très rapidement qu’on a gagné du temps d’exécution :

findMetadata2

2 – Symfony : Optimisation du routeur

Ça c’était l’optimisation facile, maintenant passons à une optimisation un peu plus pointue, qui aurait été très complexe à mettre en oeuvre sans blackfire.

Pour bien comprendre le but de cette optimisation, il faut comprendre le fonctionnement du système de routing, en tous cas tel que mis en oeuvre dans l’édition standard de symfony.
Les routes sont créées par les développeurs dans des fichiers de configuration – le plus souvent au format Yml ou XML ou, directement dans les contrôleurs sous forme d’annotations. Pour éviter un coût important de parsing, lors de la construction du cache, Symfony récupère l’intégralité des routes déclarées et crée une classe de « matching » qui implémente une méthode Match. Ainsi, en faisant appel à une seule méthode d’une seule classe, on est capables d’effectuer le dispatch de l’uri récupérée en entrée. C’est cette classe générée que j’ai cherché à optimiser.

Tout d’abord j’ai commencé par me créer un petit script de test : je récupère le fichier dumpé par Symfony (dans un environnement de dév ça se trouve ici : app/cache/dev/appDevUrlMatcher.php), instancie la classe et lui demande d’effectuer quelques matchs. Je lance tout ça en ligne de commande avec « blackfire run php test1.php » et bim, blackfire effectue l’analyse et envoie les résultats à la plateforme saas.

Premier fichier de test :

Capture d'écran de 2015-12-01 14:39:13

Premier résultat sur blackfire :

blackfire-test1

Bon je vous montre pas la totale, mais il y a du monde là dedans. Pour améliorer tout ça, j’ai décidé de commencer à jouer avec le sdk de blackfire.

Pratique : Blackfire offre une interface vers son extension php avec le package composer : blackfire/php-sdk. Un coup de require et le tour est joué ! Voici que je me décide à cette fois-ci déclencher l’instanciation dans mon script avec ce genre de choses :

Capture d'écran de 2015-12-01 14:52:05

Dans ce script, bien évidemment $matcher fait référence à la classe de Matching de symfony. Il me suffit désormais de lancer le test avec « php test.php » et voici mes résultats qui apparaissent dans la timeline de blackfire !

test4

Ok, on commence à s’y répérer. On voit donc que dans cet exemple, on a environ 380 000 appels à strpos et que ça consomme 23% du temps total. Et oui, souvenez-vous, tant qu’une fonction ne fait pas trop parler d’elle on en entend pas parler dans blackfire. Cette fois ce n’est pas le cas, chaque appel est unitairement peu coûteux mais au total, ça peut finir par représenter du temps !

 Note : vous pouvez retrouver les différents scripts d’exemple, ainsi que les différentes versions de la classe de Matching dans ce repo: https://github.com/xavierleune/symfony_urlmatcher

J’ai donc commencé à vouloir différencier les routes statiques des routes dynamiques. En regroupant les routes statiques dans un tableau avec comme clé l’url statique, un simple isset() fait la blague. Un nouveau profiling (voir test_priority_sans_preg.php) m’a donné ceci :

test_priority_sans_preg

J’ai gagné quelques appels à strpos mais j’ai perdu en CPU (sauf sur le matching de route statique). Evidemment, les applications modernes ont souvient bien plus de routes dynamiques que de routes statiques. Ca ne fait donc pas tout à fait le travail et on peut aller plus loin. J’ai donc repensé à ce blog post de nikita popov : Fast request routing using regular expressions dans lequel il explique qu’il vaut mieux faire des appels à preg_match pour les urls dynamique en regroupant les matching possible dans une grosse regex (matchant environt une 10 aine de route).

J’ai donc décidé de réimplémenter la même logique dans le Matcher de Symfony. Quelques (longues) heures plus tard (et oui, pas simple d’utiliser du PHP pour générer des classes PHP), voici les résultats de mon dernier test :

test_priority_avec_preg

Et parce que c’est pas forcément très parlant, voici la sortie du script original (test4.php) et du script optimisé :

Capture d'écran de 2015-12-01 15:20:19

On a donc une réelle optimisation dans la plupart des cas d’études dans ce contexte.

Le code est plus ou moins à l’état de POC mais une PR est malgré tout ouverte sur symfony: https://github.com/symfony/symfony/pull/16786

Il y a encore des choses à améliorer, probablement pour limiter la légère perte de performances sur le WorstCase.

Vous aimerez aussi...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *