Profiler en PHP

Divulgâchage : Lorsque vous avez besoin de mesurer les ressources prises par certaines parties de vos scripts, plutôt que de sortir la grosse artillerie avec xDebug, je vous proposes une petite classe bien pratique. On va créer une petite classe qui chronomètre le temps entre constructeur et destructeurs et émet son message à ce moment là. L’intérêt est qu’on peut mesurer une fonction avec une seule ligne de code (plus facile à insérer, et à nettoyer).

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…

PublicDomainPicture @ pixabay

Utiliser Xdebug. Sortir le grand jeu en utilisant Xdebug, 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 certes très précise et pratique, mais parfois, on veut juste avoir le temps ou la mémoire utilisée par deux pauvres fonctions.

Utiliser des rustines. 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 :

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 sachant qu’à 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 facilement. 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 langages.

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

    if ($size > 0) {
        // en php, cette reaffectation détruit l'objet précédent
        $profiler = new Profiler("Memory") ;
        // Profiler profiler("Memory") ; en C++
        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 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éant l’utilité de la classe. À 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 à la règle une fonction fait une seule chose, ce problème ne se pose jamais.

Et après ?

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.