Vu que ça intéresse du monde, et même si on attend la zone floue entre plateforme et appli (ceci étant dit, je suis partant pour un grand débat sur la séparation des pouvoirs entre système et dev), voilà la version "rustique" de la chose :
La config PHP :
[xhprof] extension=xhprof.so xhprof.output_dir=[répertoire temporaire où sont stockés les traces]
En tout début de code ou dans un fichier à part, chargé en auto_prepend_file (attention, tout ce qui est exécuté avant n'est pas profilé) - je ne trace que ce qui est en HTTP (d'où le isset($_SERVER...), et je ne trace pas les fonctions PHP (XHPROF_FLAGS_NO_BUILTINS) pour ne pas trop surcharger les traces :
if (function_exists('xhprof_enable') && isset($_SERVER['HTTP_HOST']) && mt_rand(1, XXX) == 1) { function save_xhprof_data() { $xhprof_data = xhprof_disable();
include_once CODE_BASE . "lib/xhprof/utils/xhprof_lib.php"; include_once CODE_BASE . "lib/xhprof/utils/xhprof_runs.php"; $xhprof_runs = new XHProfRuns_Default(); $run_id = $xhprof_runs->save_run($xhprof_data, "xhprof_data"); } xhprof_enable(XHPROF_FLAGS_NO_BUILTINS + XHPROF_FLAGS_MEMORY); register_shutdown_function('save_xhprof_data'); }
NB : - les libs xhprof_xxx sont founies avec xhprof, remplacer le CODE_BASE par l'endroit où sont placées ces libs. - remplacer le XXX du mt_rand par la valeur qui vous permet d'avoir quelques traces par minute
Pour l'analyse :
xhprof_aggregate.php - croner xhprof_aggregate.php toutes les nuits pour consolider les données de la veille - lancer xhprof_aggregate.php simul pour simuler l'exécution et afficher le rapport en cours
Ca sort tout sur la sortie standard, ça peut donc se rediriger dans un log, ou générer un mail, comme toute cron...
Je vous laisse vous inspirer de tout ça pour faire quelque chose de plus industriel (fusion de logs de plusieurs serveurs, stockage des données en base, etc.)
#!/usr/bin/php <?php
define('SEUIL_DELTA', 20); define('NB_TOP', 20);
$output_dir = ini_get("xhprof.output_dir");
$dir = new DirectoryIterator($output_dir);
include_once CODE_BASE . "lib/xhprof/utils/xhprof_lib.php"; include_once CODE_BASE . "lib/xhprof/utils/xhprof_runs.php"; include_once CODE_BASE . "lib/xhprof/display/xhprof.php";
$simul = (count($argv) > 1 && $argv[1] == "simul");
// On prend tous les fichiers de run présents dans le répertoire // qu'on va ensuite agréger
$aggregatedfile = $output_dir."/".date("Ymd").".aggregated"; if (!$simul && file_exists($aggregatedfile)) { $aggregated = unserialize(file_get_contents($aggregatedfile)); $files = array(); } else { foreach ($dir as $file) { if ($file->isFile() && preg_match('/([a-z0-9]+).xhprof_data/', $file->getBasename(), $matches)) { $run_ids[] = $matches[1]; $files[] = $file->getPathname(); } } if (count($run_ids) == 0) { die("Pas de run à analyser"); }
$xhprof_runs = new XHProfRuns_Default(); $aggregated = xhprof_aggregate_runs($xhprof_runs, $run_ids, array(), "xhprof_data"); if (!$simul) { file_put_contents($aggregatedfile, serialize($aggregated)); } }
$computed = xhprof_compute_flat_info($aggregated['raw'], $total); //print_r($computed);
// Calcul des tops
function displayTop($computed, $key, $scale = 1000) { $slice = array_slice($computed, 0, NB_TOP); $max = null; foreach($slice as $call => $data) { $val = round($data[$key]/$scale); if ($max == null) $max = $val; printf("%s : %d (count %d / avg %.2f / %d %%)\n", $call, $val, $data['ct'], $val/$data['ct'], 100*$val/$max); } } // Top par wall-time (self) function compareWT($a, $b) { global $computed; return $computed[$a]['excl_wt'] < $computed[$b]['excl_wt']; } uksort($computed, "compareWT"); print "\nTop 10 (temps self/msec)\n"; displayTop($computed, 'excl_wt');
// Top par wall-time (total) function compareWT2($a, $b) { global $computed; return $computed[$a]['wt'] < $computed[$b]['wt']; } uksort($computed, "compareWT2"); print "\nTop 10 (temps incl/msec)\n"; displayTop($computed, 'wt');
// Top par mémoire utilisée (self) function compareMU($a, $b) { global $computed; return $computed[$a]['excl_mu'] < $computed[$b]['excl_mu']; } uksort($computed, "compareMU"); print "\nTop 10 (memory self/Mo)\n"; displayTop($computed, 'excl_mu', 1024*1024);
// Calcul des diff par rapport à hier // On ne prend que le top des entrées
function delta($before, $today_pruned) { global $computed;
$delta = false; $computed = xhprof_compute_flat_info($before['raw'], $total); uksort($computed, "compareWT"); $before_pruned = array_slice($computed, 0, NB_TOP); //$before_pruned = xhprof_compute_flat_info(xhprof_prune_run($before['raw'], SEUIL_DELTA), $total); foreach ($today_pruned as $fn => $data) { if (!isset($before_pruned[$fn])) { //print "Apparition : $fn (temps incl ".$data['wt']."/self ".$data['excl_wt'].")\n"; $val = round($data['excl_wt']/1000); printf("Apparition de %s : self %d (count %d / avg %.2f)\n", $fn, $val, $data['ct'], $val/$data['ct']); $delta = true; } else { $beforedata = $before_pruned[$fn]; //print "Déjà présent : $fn (temps incl ".$data['wt']."/self ".$data['excl_wt'].")\n"; $val = round($data['excl_wt']/1000); $beforeval = round($beforedata['excl_wt']/1000); printf("Déjà présent %s : self %d (count %d / avg %.2f vs %.2f)\n", $fn, $val, $data['ct'], $val/$data['ct'], $beforeval/$beforedata['ct']); } } foreach ($before_pruned as $fn => $data) { if (!isset($today_pruned[$fn])) { //print "Disparition : $fn (temps incl ".$data['wt']."/self ".$data['excl_wt'].")\n"; $val = round($data['excl_wt']/1000); printf("Disparition de %s : self %d (count %d / avg %.2f)\n", $fn, $val, $data['ct'], $val/$data['ct']); $delta = true; } } return $delta; }
uksort($computed, "compareWT"); $today_pruned = array_slice($computed, 0, NB_TOP);
$yesterdayfile = $output_dir."/".date("Ymd", strtotime("-1 day")).".aggregated"; if (file_exists($yesterdayfile)) { print "\nDelta par rapport à hier :\n"; $before = unserialize(file_get_contents($yesterdayfile)); delta($before, $today_pruned); }
$lastweekfile = $output_dir."/".date("Ymd", strtotime("-1 week")).".aggregated"; if (file_exists($lastweekfile)) { print "\nDelta par rapport à la semaine dernière :\n"; $before = unserialize(file_get_contents($lastweekfile)); delta($before, $today_pruned); }
if (!$simul) { foreach($files as $path) { unlink($path); } }
NB: - il est possible qu'il reste des dépendances par rapport au reste de l'appli (même si un nettoyage a été fait), et que ça ne demande donc des adaptations pour marcher chez vous - le pourcentage affiché est par rapport à la fonction qui consomme le plus, pas par rapport au temps global d'exécution (qu'on ne connait pas), histoire d'avoir des ordres de comparaison - si vous n'arrivez pas à faire marcher le code, c'est que c'était une mauvaise idée d'essayer
JFB