MDL-29029 move profiling to admin tools
[moodle.git] / lib / xhprof / xhprof_moodle.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * @package    core
19  * @subpackage profiling
20  * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 defined('MOODLE_INTERNAL') || die();
26 // need some stuff from xhprof
27 require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_lib.php');
28 require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php');
29 // need some stuff from moodle
30 require_once($CFG->libdir.'/tablelib.php');
32 // TODO: Change the implementation below to proper profiling class
34 /**
35  * Returns if profiling is running, optionally setting it
36  */
37 function profiling_is_running($value = null) {
38     static $running = null;
40     if (!is_null($value)) {
41         $running = (bool)$value;
42     }
44     return $running;
45 }
47 /**
48  * Returns if profiling has been saved, optionally setting it
49  */
50 function profiling_is_saved($value = null) {
51     static $saved = null;
53     if (!is_null($value)) {
54         $saved = (bool)$value;
55     }
57     return $saved;
58 }
60 /**
61  * Start profiling observing all the configuration
62  */
63 function profiling_start() {
64     global $CFG, $SESSION, $SCRIPT;
66     // If profiling isn't available, nothing to start
67     if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
68         return false;
69     }
71     // If profiling isn't enabled, nothing to start
72     if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
73         return false;
74     }
76     // If profiling is already running or saved, nothing to start
77     if (profiling_is_running() || profiling_is_saved()) {
78         return false;
79     }
81     // Set script (from global if available, else our own)
82     $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
84     // Get PGC variables
85     $check = 'PROFILEME';
86     $profileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
87     $profileme = $profileme && !empty($CFG->profilingallowme);
88     $check = 'DONTPROFILEME';
89     $dontprofileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
90     $dontprofileme = $dontprofileme && !empty($CFG->profilingallowme);
91     $check = 'PROFILEALL';
92     $profileall = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
93     $profileall = $profileall && !empty($CFG->profilingallowall);
94     $check = 'PROFILEALLSTOP';
95     $profileallstop = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
96     $profileallstop = $profileallstop && !empty($CFG->profilingallowall);
98     // DONTPROFILEME detected, nothing to start
99     if ($dontprofileme) {
100         return false;
101     }
103     // PROFILEALLSTOP detected, clean the mark in seesion and continue
104     if ($profileallstop && !empty($SESSION)) {
105         unset($SESSION->profileall);
106     }
108     // PROFILEALL detected, set the mark in session and continue
109     if ($profileall && !empty($SESSION)) {
110         $SESSION->profileall = true;
112     // SESSION->profileall detected, set $profileall
113     } else if (!empty($SESSION->profileall)) {
114         $profileall = true;
115     }
117     // Evaluate automatic (random) profiling if necessary
118     $profileauto = false;
119     if (!empty($CFG->profilingautofrec)) {
120         $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
121     }
123     // See if the $script matches any of the included patterns
124     $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
125     $profileincluded = profiling_string_matches($script, $included);
127     // See if the $script matches any of the excluded patterns
128     $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
129     $profileexcluded = profiling_string_matches($script, $excluded);
131     // Decide if profile auto must happen (observe matchings)
132     $profileauto = $profileauto && $profileincluded && !$profileexcluded;
134     // Decide if profile by match must happen (only if profileauto is disabled)
135     $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);
137     // If not auto, me, all, match have been detected, nothing to do
138     if (!$profileauto && !$profileme && !$profileall && !$profilematch) {
139         return false;
140     }
142     // Arrived here, the script is going to be profiled, let's do it
143     $ignore = array('call_user_func', 'call_user_func_array');
144     xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
145     profiling_is_running(true);
147     // Started, return true
148     return true;
151 /**
152  * Stop profiling, gathering results and storing them
153  */
154 function profiling_stop() {
155     global $CFG, $DB, $SCRIPT;
157     // If profiling isn't available, nothing to stop
158     if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
159         return false;
160     }
162     // If profiling isn't enabled, nothing to stop
163     if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
164         return false;
165     }
167     // If profiling is not running or is already saved, nothing to stop
168     if (!profiling_is_running() || profiling_is_saved()) {
169         return false;
170     }
172     // Set script (from global if available, else our own)
173     $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
175     // Arrived here, profiling is running, stop and save everything
176     profiling_is_running(false);
177     $data = xhprof_disable();
179     // We only save the run after ensuring the DB table exists
180     // (this prevents problems with profiling runs enabled in
181     // config.php before Moodle is installed. Rare but...
182     $tables = $DB->get_tables();
183     if (!in_array('profiling', $tables)) {
184         return false;
185     }
187     $run = new moodle_xhprofrun();
188     $run->prepare_run($script);
189     $runid = $run->save_run($data, null);
190     profiling_is_saved(true);
192     // Prune old runs
193     profiling_prune_old_runs($runid);
195     // Finished, return true
196     return true;
199 function profiling_prune_old_runs($exception = 0) {
200     global $CFG, $DB;
202     // Setting to 0 = no prune
203     if (empty($CFG->profilinglifetime)) {
204         return;
205     }
207     $cuttime = time() - ($CFG->profilinglifetime * 60);
208     $params = array('cuttime' => $cuttime, 'exception' => $exception);
210     $DB->delete_records_select('profiling', 'runreference = 0 AND
211                                              timecreated < :cuttime AND
212                                              runid != :exception', $params);
215 /**
216  * Returns the path to the php script being requested
217  *
218  * Note this function is a partial copy of initialise_fullme() and
219  * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
220  * friends. To be used by early profiling runs in situations where
221  * $SCRIPT isn't defined yet
222  *
223  * @return string absolute path (wwwroot based) of the script being executed
224  */
225 function profiling_get_script() {
226     global $CFG;
228     $wwwroot = parse_url($CFG->wwwroot);
230     if (!isset($wwwroot['path'])) {
231         $wwwroot['path'] = '';
232     }
233     $wwwroot['path'] .= '/';
235     $path = $_SERVER['SCRIPT_NAME'];
237     if (strpos($path, $wwwroot['path']) === 0) {
238         return substr($path, strlen($wwwroot['path']) - 1);
239     }
240     return '';
243 function profiling_urls($report, $runid, $runid2 = null) {
244     global $CFG;
246     $url = '';
247     switch ($report) {
248         case 'run':
249             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
250             break;
251         case 'diff':
252             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
253             break;
254         case 'graph':
255             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
256             break;
257     }
258     return $url;
261 function profiling_print_run($run, $prevrunid = null) {
262     global $CFG, $OUTPUT;
264     $output = '';
266     // Prepare the runreference/runcomment form
267     $checked = $run->runreference ? ' checked=checked' : '';
268     $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
269                      "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
270                      "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
271                      "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
272                      "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
273                      "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
274                      "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
275                      "</form>";
277     $table = new html_table();
278     $table->align = array('right', 'left');
279     $table->tablealign = 'center';
280     $table->attributes['class'] = 'profilingruntable';
281     $table->colclasses = array('label', 'value');
282     $table->data = array(
283        array(get_string('runid', 'tool_profiling'), $run->runid),
284        array(get_string('url'), $run->url),
285        array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
286        array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
287        array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
288        array(get_string('calls', 'tool_profiling'), $run->totalcalls),
289        array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
290        array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
291     $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary', true);
292     // Add link to details
293     $strviewdetails = get_string('viewdetails', 'tool_profiling');
294     $url = profiling_urls('run', $run->runid);
295     $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
296                               'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
297     // If there is one previous run marked as reference, add link to diff
298     if ($prevrunid) {
299         $strviewdiff = get_string('viewdiff', 'tool_profiling');
300         $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $prevrunid . '&amp;listurl=' . urlencode($run->url);
301         $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strviewdiff . '</a>', 3, 'main profilinglink');
302     }
304     return $output;
307 function profiling_print_rundiff($run1, $run2) {
308     global $CFG, $OUTPUT;
310     $output = '';
312     // Prepare the reference/comment information
313     $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
314                       ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
315     $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
316                       ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
318     // Calculate global differences
319     $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
320     $diffcputime       = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
321     $diffcalls         = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
322     $diffmemory        = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
324     $table = new html_table();
325     $table->align = array('right', 'left', 'left', 'left');
326     $table->tablealign = 'center';
327     $table->attributes['class'] = 'profilingruntable';
328     $table->colclasses = array('label', 'value1', 'value2');
329     $table->data = array(
330        array(get_string('runid', 'tool_profiling'),
331            '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
332            '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
333        array(get_string('url'), $run1->url, $run2->url),
334        array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
335            userdate($run2->timecreated, '%d %B %Y, %H:%M')),
336        array(get_string('executiontime', 'tool_profiling'),
337            format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
338            format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
339        array(get_string('cputime', 'tool_profiling'),
340            format_float($run1->totalcputime / 1000, 3) . ' ms',
341            format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
342        array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
343        array(get_string('memory', 'tool_profiling'),
344            format_float($run1->totalmemory / 1024, 0) . ' KB',
345            format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
346        array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
347     $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary', true);
348     // Add link to details
349     $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
350     $url = profiling_urls('diff', $run1->runid, $run2->runid);
351     //$url =  $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
352     $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
353                               'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
354     return $output;
357 /**
358  * Helper function that returns the HTML fragment to
359  * be displayed on listing mode, it includes actions
360  * like deletion/export/import...
361  */
362 function profiling_list_controls($listurl) {
363     global $CFG, $OUTPUT;
365     $output = '';
367     return $output;
370 /**
371  * Helper function that looks for matchings of one string
372  * against an array of * wildchar patterns
373  */
374 function profiling_string_matches($string, $patterns) {
375     $patterns = explode(',', $patterns);
376     foreach ($patterns as $pattern) {
377         // Trim and prepare pattern
378         $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
379         // Don't process empty patterns
380         if (empty($pattern)) {
381             continue;
382         }
383         if (preg_match('~' . $pattern . '~', $string)) {
384             return true;
385         }
386     }
387     return false;
390 /**
391  * Helper function that, given to floats, returns their numerical
392  * and percentual differences, propertly formated and cssstyled
393  */
394 function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
395     $numdiff = $number2 - $number1;
396     $perdiff = 0;
397     if ($number1 != $number2) {
398         $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
399     }
400     $sign      = $number2 > $number1 ? '+' : '';
401     $delta     = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
402     $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
403     $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
404     $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
405     $endspan   = '</span>';
406     $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
407     $fperdiff = $sign . format_float($perdiff, $numdec);
408     return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
411 /**
412  * Custom implementation of iXHProfRuns
413  *
414  * This class is one implementation of the iXHProfRuns interface, in charge
415  * of storing and retrieve profiling run data to/from DB (profiling table)
416  *
417  * The interface only defines two methods to be defined: get_run() and
418  * save_run() we'll be implementing some more in order to keep all the
419  * rest of information in our runs properly handled.
420  */
421 class moodle_xhprofrun implements iXHProfRuns {
423     protected $runid = null;
424     protected $url = null;
425     protected $totalexecutiontime = 0;
426     protected $totalcputime = 0;
427     protected $totalcalls = 0;
428     protected $totalmemory = 0;
429     protected $timecreated = 0;
431     public function __construct() {
432         $this->timecreated = time();
433     }
435     /**
436      * Given one runid and one type, return the run data
437      * and some extra info in run_desc from DB
438      *
439      * Note that $type is completely ignored
440      */
441     public function get_run($run_id, $type, &$run_desc) {
442         global $DB;
444         $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
446         $this->runid = $rec->runid;
447         $this->url = $rec->url;
448         $this->totalexecutiontime = $rec->totalexecutiontime;
449         $this->totalcputime = $rec->totalcputime;
450         $this->totalcalls = $rec->totalcalls;
451         $this->totalmemory = $rec->totalmemory;
452         $this->timecreated = $rec->timecreated;
454         $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
456         return unserialize(base64_decode($rec->data));
457     }
459     /**
460      * Given some run data, one type and, optionally, one runid
461      * store the information in DB
462      *
463      * Note that $type is completely ignored
464      */
465     public function save_run($xhprof_data, $type, $run_id = null) {
466         global $DB;
468         if (is_null($this->url)) {
469             xhprof_error("Warning: You must use the prepare_run() method before saving it");
470         }
472         // Calculate runid if needed
473         $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
475         // Calculate totals
476         $this->totalexecutiontime = $xhprof_data['main()']['wt'];
477         $this->totalcputime = $xhprof_data['main()']['cpu'];
478         $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
479         $this->totalmemory = $xhprof_data['main()']['mu'];
481         // Prepare data
482         $rec = new stdClass();
483         $rec->runid = $this->runid;
484         $rec->url = $this->url;
485         $rec->data = base64_encode(serialize($xhprof_data));
486         $rec->totalexecutiontime = $this->totalexecutiontime;
487         $rec->totalcputime = $this->totalcputime;
488         $rec->totalcalls = $this->totalcalls;
489         $rec->totalmemory = $this->totalmemory;
490         $rec->timecreated = $this->timecreated;
492         $DB->insert_record('profiling', $rec);
493         return $this->runid;
494     }
496     public function prepare_run($url) {
497         $this->url = $url;
498     }
500     // Private API starts here
502     protected function sum_calls($sum, $data) {
503         return $sum + $data['ct'];
504     }
507 /**
508  * Simple subclass of {@link table_sql} that provides
509  * some custom formatters for various columns, in order
510  * to make the main profiles list nicer
511  */
512 class xhprof_table_sql extends table_sql {
514     protected $listurlmode = false;
516     /**
517      * Get row classes to be applied based on row contents
518      */
519     function get_row_class($row) {
520         return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
521     }
523     /**
524      * Define it the table is in listurlmode or not, output will
525      * be different based on that
526      */
527     function set_listurlmode($listurlmode) {
528         $this->listurlmode = $listurlmode;
529     }
531     /**
532      * Format URL, so it points to last run for that url
533      */
534     protected function col_url($row) {
535         global $OUTPUT;
537         // Build the link to latest run for the script
538         $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
539         $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
541         // Decide, based on $this->listurlmode which actions to show
542         if ($this->listurlmode) {
543             $detailsaction = '';
544         } else {
545             // Build link icon to script details (pix + url + actionlink)
546             $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
547             $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
548             $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
549         }
551         return $scriptaction . '&nbsp;' . $detailsaction;
552     }
554     /**
555      * Format profiling date, human and pointing to run
556      */
557     protected function col_timecreated($row) {
558         global $OUTPUT;
559         $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
560         $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
561         return $OUTPUT->action_link($url, $fdate);
562     }
564     /**
565      * Format execution time
566      */
567     protected function col_totalexecutiontime($row) {
568         return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
569     }
571     /**
572      * Format cpu time
573      */
574     protected function col_totalcputime($row) {
575         return format_float($row->totalcputime / 1000, 3) . ' ms';
576     }
578     /**
579      * Format memory
580      */
581     protected function col_totalmemory($row) {
582         return format_float($row->totalmemory / 1024, 3) . ' KB';
583     }