Profiler en PHP

tbowan

25/06/2017

Lorsque vous avez besoin de mesurer les ressources prises par certaines parties de vos scripts, plutôt que de sortir la grosse artilerie avec xDebug, je vous proposes une petite classe bien pratique.

Introduction

Parfois, ou plutôt souvent en fait, vous vous retrouvez à vouloir mesurer les ressources utilisées par vos scripts PHP. La plupart du temps, vous avez votre application qui répond [trop] lentement, vous avez une idée de la partie qui pose problème mais vous voulez mesurer un peu histoire d'être sûr.

Se pose alors, dans les grandes lignes, deux solutions :

  1. Sortir le grand jeu en utilisant Xdebug1, vous saurez alors exactement quelles portions de code prennent combien de temps, par où vous passez, ...

Je trouve cette solution un peu lourde et peu adaptées aux applications en production. Elle est certe très précise et pratique, mais parfois, on veut juste avoir le temps ou la mémoire utilisée par deux pauvres fonctions.

  1. Faire un petit patch, qu'il faudra annuler ensuite ; un appel à microtime avant / après et on écrit la différence quelque part.

Mais cette fois, je trouve toujours un peu pénible de devoir écrire les mêmes lignes à chaque fois. D'abord parce qu'avec une seule ligne ça serait bien plus propre. Ensuite, parce qu'à force d'en ajouter petit à petit pour trouver où est le problème, on pourrit le code qu'il faut ensuite nettoyer.

Je vous proposes ici une troisième solution à mi-chemin qui m'a bien aidée à de nombreuses reprises.

Du RAII pour faire des mesures

La solution que j'ai trouvée pour résoudre ce problème tire parti du RAII pattern (dont je vous ai déjà parlé):

Vu que le PHP supprime les objets dès qu'ils ne sont plus référencés, vous pouvez créer un objet en début de fonction et à la fin de son exécution, le destructeur sera appelé et vous aurez votre chrono. Le tout, en une seule ligne de code ce qui réduit la fatigue (et surtout les problèmes).

Lorsque vous voudrez supprimer vos lignes, vous pourrez les retrouver facilements. Soit en supprimant la classe et en regardant quelles lignes posent problèmes dans vos tests (mais pour ça, il faut des tests auto...). Soit en utilisant sed pour supprimer automatiquement les lignes, en adaptant l'exemple suivant :

sed -i '/new Profiler/d' $(find ./src -name "*.php")

Le code

Le but étant d'avoir un système léger et facile à utiliser, la classe que je vous propose est volontairement réduite au minimum. Plutôt que de développer quelque chose de très générique (avec des callbacks pour l'émission du log par exemple), je préfère généralement ajouter cette classe lorsque j'en ai besoin en l'adaptant aux situations.

class Profiler
{

    private $label ;
    private $time ;

    public function __construct($label = "")
    {
        $this->label    = $label ;
        $this->time     = microtime(true) ;
    }

    public function __destruct()
    {
        $msg = sprintf("%s : %f %f",
            $this->label,
            microtime(true) - $this->time,
            memory_get_peak_usage()
            ) ;

        error_log($msg) ;
    }

}

Utilisation

Comme vous avez pu l'anticiper, l'utilisation du profiler pour obtenir les mesures dans vos logs consiste juste à créer un objet en début de fonction.

Voici un exemple de fonction, dont le but est uniquement d'illustrer l'utilisation du Profiler en consommant inutilement des ressources. Vous pouvez voir la ligne qui crée l'objet, suffisante pour faire tout le boulot.

function consumeMemory($size)
{
    $profiler = new Profiler("test alloc");
    $t = [] ;
    for ($i = 0; $i < $size; $i++) {
        $t[] = $t ;
    }
    // $profiler est détruit ici automatiquement
}

Restriction

Contrairement au C++ où les objets sont détruits lorsqu'on quitte un bloc, le PHP les détruits lorsqu'ils ne sont plus référencés. C'est à dire à la fin des fonctions et non plus de n'importe quel bloc, ce qui introduit une subtile différence.

L'exemple suivant, toujours dans un but illustratif, montre un cas où le comportement est différent entre les deux languages.

function consume($time, $size)
{
    if ($time > 0) {
        $profiler = new Profiler("Time") ;
        sleep($time) ;
        // en c++, l'objet est détruit ici
    }
    // en PHP, il existe encore ici

    if ($size > 0) {
        // en php, cette reaffectation détruit l'objet précédent
        $profiler = new Profiler("Memory") ;
        consumeMemory($size) ;
        // en C++, l'objet est détruit ici
    }

    sleep($time + $size) ; // ou autre calcul gourmand

    // en PHP, l'objet n'est détruit qu'ici
}

Si vous pensiez mesurer chaque bloc if individuellement, comme c'est le cas en C++, vous vous trompez. Les $profiler survivent à la fin des blocs et le dernier créé mesurera également tout le reste de l'exécution de la fonction et vous donnera donc des mesures non intuitives.

Pour remédier au problème, vous pourriez ajouter des unset $profiler ; mais ça impose d'écrire deux lignes et surtout, ça réduit à néan l'utilité de la classe. A ce compte là, autant faire une méthode close() à la classe Profiler et arrêter ces histoires de RAII.

Personnellement, je considère que c'est se tromper de problème. Si vous en êtes rendu à mesurer des portions de fonction complexes comme celle-ci, ce n'est pas le Profiler qui est en cause mais la fonction qu'il faut reprendre. En me restreignant à des fonctions de 10 lignes de code et en appliquant la règle "une fonction fait une seule chose", ce problème ne se pose jamais.

Conclusion

Le RAII pattern, c'est un peu comme la respiration, une fois qu'on s'y est mis, on ne peut plus s'en passer. Très pratique pour les ressources et les mutex, il peut aussi rendre pas mal de services dès qu'il s'agit d'un comportement à effectuer en fin de fonction.

Il faut cependant être prudent car le PHP a quelques subtiles différences avec le C++ qu'il faut avoir en tête lorsqu'on utilise des objets RAIIsés si on ne veut pas avoir de (mauvaises) surprises.