MDL-55292 performance: Allow Tideways profiler extension.
[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');
31 require_once($CFG->libdir . '/setuplib.php');
32 require_once($CFG->libdir . '/phpunit/classes/util.php');
33 require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
34 require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
35 require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');
37 // TODO: Change the implementation below to proper profiling class.
39 /**
40  * Returns if profiling is running, optionally setting it
41  */
42 function profiling_is_running($value = null) {
43     static $running = null;
45     if (!is_null($value)) {
46         $running = (bool)$value;
47     }
49     return $running;
50 }
52 /**
53  * Returns if profiling has been saved, optionally setting it
54  */
55 function profiling_is_saved($value = null) {
56     static $saved = null;
58     if (!is_null($value)) {
59         $saved = (bool)$value;
60     }
62     return $saved;
63 }
65 /**
66  * Start profiling observing all the configuration
67  */
68 function profiling_start() {
69     global $CFG, $SESSION, $SCRIPT;
71     // If profiling isn't available, nothing to start
72     if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
73         return false;
74     }
76     // If profiling isn't enabled, nothing to start
77     if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
78         return false;
79     }
81     // If profiling is already running or saved, nothing to start
82     if (profiling_is_running() || profiling_is_saved()) {
83         return false;
84     }
86     // Set script (from global if available, else our own)
87     $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
89     // Get PGC variables
90     $check = 'PROFILEME';
91     $profileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
92     $profileme = $profileme && !empty($CFG->profilingallowme);
93     $check = 'DONTPROFILEME';
94     $dontprofileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
95     $dontprofileme = $dontprofileme && !empty($CFG->profilingallowme);
96     $check = 'PROFILEALL';
97     $profileall = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
98     $profileall = $profileall && !empty($CFG->profilingallowall);
99     $check = 'PROFILEALLSTOP';
100     $profileallstop = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
101     $profileallstop = $profileallstop && !empty($CFG->profilingallowall);
103     // DONTPROFILEME detected, nothing to start
104     if ($dontprofileme) {
105         return false;
106     }
108     // PROFILEALLSTOP detected, clean the mark in seesion and continue
109     if ($profileallstop && !empty($SESSION)) {
110         unset($SESSION->profileall);
111     }
113     // PROFILEALL detected, set the mark in session and continue
114     if ($profileall && !empty($SESSION)) {
115         $SESSION->profileall = true;
117     // SESSION->profileall detected, set $profileall
118     } else if (!empty($SESSION->profileall)) {
119         $profileall = true;
120     }
122     // Evaluate automatic (random) profiling if necessary
123     $profileauto = false;
124     if (!empty($CFG->profilingautofrec)) {
125         $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
126     }
128     // See if the $script matches any of the included patterns
129     $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
130     $profileincluded = profiling_string_matches($script, $included);
132     // See if the $script matches any of the excluded patterns
133     $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
134     $profileexcluded = profiling_string_matches($script, $excluded);
136     // Decide if profile auto must happen (observe matchings)
137     $profileauto = $profileauto && $profileincluded && !$profileexcluded;
139     // Decide if profile by match must happen (only if profileauto is disabled)
140     $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);
142     // If not auto, me, all, match have been detected, nothing to do
143     if (!$profileauto && !$profileme && !$profileall && !$profilematch) {
144         return false;
145     }
147     // Arrived here, the script is going to be profiled, let's do it
148     $ignore = array('call_user_func', 'call_user_func_array');
149     if (extension_loaded('tideways')) {
150         tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
151     } else {
152         xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
153     }
154     profiling_is_running(true);
156     // Started, return true
157     return true;
160 /**
161  * Stop profiling, gathering results and storing them
162  */
163 function profiling_stop() {
164     global $CFG, $DB, $SCRIPT;
166     // If profiling isn't available, nothing to stop
167     if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
168         return false;
169     }
171     // If profiling isn't enabled, nothing to stop
172     if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
173         return false;
174     }
176     // If profiling is not running or is already saved, nothing to stop
177     if (!profiling_is_running() || profiling_is_saved()) {
178         return false;
179     }
181     // Set script (from global if available, else our own)
182     $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
184     // Arrived here, profiling is running, stop and save everything
185     profiling_is_running(false);
186     if (extension_loaded('tideways')) {
187         $data = tideways_disable();
188     } else {
189         $data = xhprof_disable();
190     }
192     // We only save the run after ensuring the DB table exists
193     // (this prevents problems with profiling runs enabled in
194     // config.php before Moodle is installed. Rare but...
195     $tables = $DB->get_tables();
196     if (!in_array('profiling', $tables)) {
197         return false;
198     }
200     $run = new moodle_xhprofrun();
201     $run->prepare_run($script);
202     $runid = $run->save_run($data, null);
203     profiling_is_saved(true);
205     // Prune old runs
206     profiling_prune_old_runs($runid);
208     // Finished, return true
209     return true;
212 function profiling_prune_old_runs($exception = 0) {
213     global $CFG, $DB;
215     // Setting to 0 = no prune
216     if (empty($CFG->profilinglifetime)) {
217         return;
218     }
220     $cuttime = time() - ($CFG->profilinglifetime * 60);
221     $params = array('cuttime' => $cuttime, 'exception' => $exception);
223     $DB->delete_records_select('profiling', 'runreference = 0 AND
224                                              timecreated < :cuttime AND
225                                              runid != :exception', $params);
228 /**
229  * Returns the path to the php script being requested
230  *
231  * Note this function is a partial copy of initialise_fullme() and
232  * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
233  * friends. To be used by early profiling runs in situations where
234  * $SCRIPT isn't defined yet
235  *
236  * @return string absolute path (wwwroot based) of the script being executed
237  */
238 function profiling_get_script() {
239     global $CFG;
241     $wwwroot = parse_url($CFG->wwwroot);
243     if (!isset($wwwroot['path'])) {
244         $wwwroot['path'] = '';
245     }
246     $wwwroot['path'] .= '/';
248     $path = $_SERVER['SCRIPT_NAME'];
250     if (strpos($path, $wwwroot['path']) === 0) {
251         return substr($path, strlen($wwwroot['path']) - 1);
252     }
253     return '';
256 function profiling_urls($report, $runid, $runid2 = null) {
257     global $CFG;
259     $url = '';
260     switch ($report) {
261         case 'run':
262             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
263             break;
264         case 'diff':
265             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
266             break;
267         case 'graph':
268             $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
269             break;
270     }
271     return $url;
274 /**
275  * Generate the output to print a profiling run including further actions you can then take.
276  *
277  * @param object $run The profiling run object we are going to display.
278  * @param array $prevreferences A list of run objects to list as comparison targets.
279  * @return string The output to display on the screen for this run.
280  */
281 function profiling_print_run($run, $prevreferences = null) {
282     global $CFG, $OUTPUT;
284     $output = '';
286     // Prepare the runreference/runcomment form
287     $checked = $run->runreference ? ' checked=checked' : '';
288     $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
289                      "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
290                      "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
291                      "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
292                      "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
293                      "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
294                      "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
295                      "</form>";
297     $table = new html_table();
298     $table->align = array('right', 'left');
299     $table->tablealign = 'center';
300     $table->attributes['class'] = 'profilingruntable';
301     $table->colclasses = array('label', 'value');
302     $table->data = array(
303        array(get_string('runid', 'tool_profiling'), $run->runid),
304        array(get_string('url'), $run->url),
305        array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
306        array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
307        array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
308        array(get_string('calls', 'tool_profiling'), $run->totalcalls),
309        array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
310        array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
311     $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
312     // Add link to details
313     $strviewdetails = get_string('viewdetails', 'tool_profiling');
314     $url = profiling_urls('run', $run->runid);
315     $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
316                                 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
318     // If there are previous run(s) marked as reference, add link to diff.
319     if ($prevreferences) {
320         $table = new html_table();
321         $table->align = array('left', 'left');
322         $table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
323         $table->tablealign = 'center';
324         $table->attributes['class'] = 'flexible generaltable generalbox';
325         $table->colclasses = array('value', 'value', 'value');
326         $table->data = array();
328         $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');
330         foreach ($prevreferences as $reference) {
331             $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $reference->runid . '&amp;listurl=' . urlencode($run->url);
332             $row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);
333             $table->data[] = $row;
334         }
335         $output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');
337     }
338     // Add link to export this run.
339     $strexport = get_string('exportthis', 'tool_profiling');
340     $url = 'export.php?runid=' . $run->runid . '&amp;listurl=' . urlencode($run->url);
341     $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
343     return $output;
346 function profiling_print_rundiff($run1, $run2) {
347     global $CFG, $OUTPUT;
349     $output = '';
351     // Prepare the reference/comment information
352     $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
353                       ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
354     $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
355                       ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
357     // Calculate global differences
358     $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
359     $diffcputime       = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
360     $diffcalls         = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
361     $diffmemory        = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
363     $table = new html_table();
364     $table->align = array('right', 'left', 'left', 'left');
365     $table->tablealign = 'center';
366     $table->attributes['class'] = 'profilingruntable';
367     $table->colclasses = array('label', 'value1', 'value2');
368     $table->data = array(
369        array(get_string('runid', 'tool_profiling'),
370            '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
371            '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
372        array(get_string('url'), $run1->url, $run2->url),
373        array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
374            userdate($run2->timecreated, '%d %B %Y, %H:%M')),
375        array(get_string('executiontime', 'tool_profiling'),
376            format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
377            format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
378        array(get_string('cputime', 'tool_profiling'),
379            format_float($run1->totalcputime / 1000, 3) . ' ms',
380            format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
381        array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
382        array(get_string('memory', 'tool_profiling'),
383            format_float($run1->totalmemory / 1024, 0) . ' KB',
384            format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
385        array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
386     $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
387     // Add link to details
388     $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
389     $url = profiling_urls('diff', $run1->runid, $run2->runid);
390     //$url =  $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
391     $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
392                               'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
393     return $output;
396 /**
397  * Helper function that returns the HTML fragment to
398  * be displayed on listing mode, it includes actions
399  * like deletion/export/import...
400  */
401 function profiling_list_controls($listurl) {
402     global $CFG;
404     $output = '<p class="centerpara buttons">';
405     $output .= '&nbsp;<a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
406     $output .= '</p>';
408     return $output;
411 /**
412  * Helper function that looks for matchings of one string
413  * against an array of * wildchar patterns
414  */
415 function profiling_string_matches($string, $patterns) {
416     $patterns = explode(',', $patterns);
417     foreach ($patterns as $pattern) {
418         // Trim and prepare pattern
419         $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
420         // Don't process empty patterns
421         if (empty($pattern)) {
422             continue;
423         }
424         if (preg_match('~' . $pattern . '~', $string)) {
425             return true;
426         }
427     }
428     return false;
431 /**
432  * Helper function that, given to floats, returns their numerical
433  * and percentual differences, propertly formated and cssstyled
434  */
435 function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
436     $numdiff = $number2 - $number1;
437     $perdiff = 0;
438     if ($number1 != $number2) {
439         $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
440     }
441     $sign      = $number2 > $number1 ? '+' : '';
442     $delta     = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
443     $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
444     $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
445     $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
446     $endspan   = '</span>';
447     $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
448     $fperdiff = $sign . format_float($perdiff, $numdec);
449     return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
452 /**
453  * Export profiling runs to a .mpr (moodle profile runs) file.
454  *
455  * This function gets an array of profiling runs (array of runids) and
456  * saves a .mpr file into destination for ulterior handling.
457  *
458  * Format of .mpr files:
459  *   mpr files are simple zip packages containing these files:
460  *     - moodle_profiling_runs.xml: Metadata about the information
461  *         exported. Contains some header information (version and
462  *         release of moodle, database, git hash - if available, date
463  *         of export...) and a list of all the runids included in the
464  *         export.
465  *    - runid.xml: One file per each run detailed in the main file,
466  *        containing the raw dump of the given runid in the profiling table.
467  *
468  * Possible improvement: Start storing some extra information in the
469  * profiling table for each run (moodle version, database, git hash...).
470  *
471  * @param array $runids list of runids to be exported.
472  * @param string $file filesystem fullpath to destination .mpr file.
473  * @return boolean the mpr file has been successfully exported (true) or no (false).
474  */
475 function profiling_export_runs(array $runids, $file) {
476     global $CFG, $DB;
478     // Verify we have passed proper runids.
479     if (empty($runids)) {
480         return false;
481     }
483     // Verify all the passed runids do exist.
484     list ($insql, $inparams) = $DB->get_in_or_equal($runids);
485     $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
486     if ($reccount != count($runids)) {
487         return false;
488     }
490     // Verify the $file path is writeable.
491     $base = dirname($file);
492     if (!is_writable($base)) {
493         return false;
494     }
496     // Create temp directory where the temp information will be generated.
497     $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
498     mkdir($tmpdir);
500     // Generate the xml contents in the temp directory.
501     $status = profiling_export_generate($runids, $tmpdir);
503     // Package (zip) all the information into the final .mpr file.
504     if ($status) {
505         $status = profiling_export_package($file, $tmpdir);
506     }
508     // Process finished ok, clean and return.
509     fulldelete($tmpdir);
510     return $status;
513 /**
514  * Import a .mpr (moodle profile runs) file into moodle.
515  *
516  * See {@link profiling_export_runs()} for more details about the
517  * implementation of .mpr files.
518  *
519  * @param string $file filesystem fullpath to target .mpr file.
520  * @param string $commentprefix prefix to add to the comments of all the imported runs.
521  * @return boolean the mpr file has been successfully imported (true) or no (false).
522  */
523 function profiling_import_runs($file, $commentprefix = '') {
524     global $DB;
526     // Any problem with the file or its directory, abort.
527     if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
528         return false;
529     }
531     // Unzip the file into temp directory.
532     $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
533     $fp = get_file_packer('application/vnd.moodle.profiling');
534     $status = $fp->extract_to_pathname($file, $tmpdir);
536     // Look for master file and verify its format.
537     if ($status) {
538         $mfile = $tmpdir . '/moodle_profiling_runs.xml';
539         if (!file_exists($mfile) or !is_readable($mfile)) {
540             $status = false;
541         } else {
542             $mdom = new DOMDocument();
543             if (!$mdom->load($mfile)) {
544                 $status = false;
545             } else {
546                 $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
547             }
548         }
549     }
551     // Verify all detail files exist and verify their format.
552     if ($status) {
553         $runs = $mdom->getElementsByTagName('run');
554         foreach ($runs as $run) {
555             $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);
556             if (!file_exists($rfile) or !is_readable($rfile)) {
557                 $status = false;
558             } else {
559                 $rdom = new DOMDocument();
560                 if (!$rdom->load($rfile)) {
561                     $status = false;
562                 } else {
563                     $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
564                 }
565             }
566         }
567     }
569     // Everything looks ok, let's import all the runs.
570     if ($status) {
571         reset($runs);
572         foreach ($runs as $run) {
573             $rfile = $tmpdir . '/' . $run->getAttribute('ref');
574             $rdom = new DOMDocument();
575             $rdom->load($rfile);
576             $runarr = array();
577             $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);
578             $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);
579             $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);
580             $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);
581             $runarr['timecreated'] = time(); // Now.
582             $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);
583             $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);
584             $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);
585             $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);
586             $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);
587             // If the runid does not exist, insert it.
588             if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
589                 $DB->insert_record('profiling', $runarr);
590             } else {
591                 return false;
592             }
593         }
594     }
596     // Clean the temp directory used for import.
597     remove_dir($tmpdir);
599     return $status;
602 /**
603  * Generate the mpr contents (xml files) in the temporal directory.
604  *
605  * @param array $runids list of runids to be generated.
606  * @param string $tmpdir filesystem fullpath of tmp generation.
607  * @return boolean the mpr contents have been generated (true) or no (false).
608  */
609 function profiling_export_generate(array $runids, $tmpdir) {
610     global $CFG, $DB;
612     // Calculate the header information to be sent to moodle_profiling_runs.xml.
613     $release = $CFG->release;
614     $version = $CFG->version;
615     $dbtype = $CFG->dbtype;
616     $githash = phpunit_util::get_git_hash();
617     $date = time();
619     // Create the xml output and writer for the main file.
620     $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
621     $mainxw = new xml_writer($mainxo);
623     // Output begins.
624     $mainxw->start();
625     $mainxw->begin_tag('moodle_profiling_runs');
627     // Send header information.
628     $mainxw->begin_tag('info');
629     $mainxw->full_tag('release', $release);
630     $mainxw->full_tag('version', $version);
631     $mainxw->full_tag('dbtype', $dbtype);
632     if ($githash) {
633         $mainxw->full_tag('githash', $githash);
634     }
635     $mainxw->full_tag('date', $date);
636     $mainxw->end_tag('info');
638     // Send information about runs.
639     $mainxw->begin_tag('runs');
640     foreach ($runids as $runid) {
641         // Get the run information from DB.
642         $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);
643         $attributes = array(
644                 'id' => $run->id,
645                 'ref' => $run->runid . '.xml');
646         $mainxw->full_tag('run', null, $attributes);
647         // Create the individual run file.
648         $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
649         $runxw = new xml_writer($runxo);
650         $runxw->start();
651         $runxw->begin_tag('moodle_profiling_run');
652         $runxw->full_tag('id', $run->id);
653         $runxw->full_tag('runid', $run->runid);
654         $runxw->full_tag('url', $run->url);
655         $runxw->full_tag('runreference', $run->runreference);
656         $runxw->full_tag('runcomment', $run->runcomment);
657         $runxw->full_tag('timecreated', $run->timecreated);
658         $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);
659         $runxw->full_tag('totalcputime', $run->totalcputime);
660         $runxw->full_tag('totalcalls', $run->totalcalls);
661         $runxw->full_tag('totalmemory', $run->totalmemory);
662         $runxw->full_tag('data', $run->data);
663         $runxw->end_tag('moodle_profiling_run');
664         $runxw->stop();
665     }
666     $mainxw->end_tag('runs');
667     $mainxw->end_tag('moodle_profiling_runs');
668     $mainxw->stop();
670     return true;
673 /**
674  * Package (zip) the mpr contents (xml files) in the final location.
675  *
676  * @param string $file filesystem fullpath to destination .mpr file.
677  * @param string $tmpdir filesystem fullpath of tmp generation.
678  * @return boolean the mpr contents have been generated (true) or no (false).
679  */
680 function profiling_export_package($file, $tmpdir) {
681     // Get the list of files in $tmpdir.
682     $filestemp = get_directory_list($tmpdir, '', false, true, true);
683     $files = array();
685     // Add zip paths and fs paths to all them.
686     foreach ($filestemp as $filetemp) {
687         $files[$filetemp] = $tmpdir . '/' . $filetemp;
688     }
690     // Get the zip_packer.
691     $zippacker = get_file_packer('application/zip');
693     // Generate the packaged file.
694     $zippacker->archive_to_pathname($files, $file);
696     return true;
699 /**
700  * Return the xml schema for the main import file.
701  *
702  * @return string
703  *
704  */
705 function profiling_get_import_main_schema() {
706     $schema = <<<EOS
707 <?xml version="1.0" encoding="UTF-8"?>
708 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
709   <xs:element name="moodle_profiling_runs">
710     <xs:complexType>
711       <xs:sequence>
712         <xs:element ref="info"/>
713         <xs:element ref="runs"/>
714       </xs:sequence>
715     </xs:complexType>
716   </xs:element>
717   <xs:element name="info">
718     <xs:complexType>
719       <xs:sequence>
720         <xs:element type="xs:string" name="release"/>
721         <xs:element type="xs:decimal" name="version"/>
722         <xs:element type="xs:string" name="dbtype"/>
723         <xs:element type="xs:string" minOccurs="0" name="githash"/>
724         <xs:element type="xs:int" name="date"/>
725       </xs:sequence>
726     </xs:complexType>
727   </xs:element>
728   <xs:element name="runs">
729     <xs:complexType>
730       <xs:sequence>
731         <xs:element maxOccurs="unbounded" ref="run"/>
732       </xs:sequence>
733     </xs:complexType>
734   </xs:element>
735   <xs:element name="run">
736     <xs:complexType>
737       <xs:attribute type="xs:int" name="id"/>
738       <xs:attribute type="xs:string" name="ref"/>
739     </xs:complexType>
740   </xs:element>
741 </xs:schema>
742 EOS;
743     return $schema;
746 /**
747  * Return the xml schema for each individual run import file.
748  *
749  * @return string
750  *
751  */
752 function profiling_get_import_run_schema() {
753     $schema = <<<EOS
754 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
755   <xs:element name="moodle_profiling_run">
756     <xs:complexType>
757       <xs:sequence>
758         <xs:element type="xs:int" name="id"/>
759         <xs:element type="xs:string" name="runid"/>
760         <xs:element type="xs:string" name="url"/>
761         <xs:element type="xs:int" name="runreference"/>
762         <xs:element type="xs:string" name="runcomment"/>
763         <xs:element type="xs:int" name="timecreated"/>
764         <xs:element type="xs:int" name="totalexecutiontime"/>
765         <xs:element type="xs:int" name="totalcputime"/>
766         <xs:element type="xs:int" name="totalcalls"/>
767         <xs:element type="xs:int" name="totalmemory"/>
768         <xs:element type="xs:string" name="data"/>
769       </xs:sequence>
770     </xs:complexType>
771   </xs:element>
772 </xs:schema>
773 EOS;
774     return $schema;
776 /**
777  * Custom implementation of iXHProfRuns
778  *
779  * This class is one implementation of the iXHProfRuns interface, in charge
780  * of storing and retrieve profiling run data to/from DB (profiling table)
781  *
782  * The interface only defines two methods to be defined: get_run() and
783  * save_run() we'll be implementing some more in order to keep all the
784  * rest of information in our runs properly handled.
785  */
786 class moodle_xhprofrun implements iXHProfRuns {
788     protected $runid = null;
789     protected $url = null;
790     protected $totalexecutiontime = 0;
791     protected $totalcputime = 0;
792     protected $totalcalls = 0;
793     protected $totalmemory = 0;
794     protected $timecreated = 0;
796     public function __construct() {
797         $this->timecreated = time();
798     }
800     /**
801      * Given one runid and one type, return the run data
802      * and some extra info in run_desc from DB
803      *
804      * Note that $type is completely ignored
805      */
806     public function get_run($run_id, $type, &$run_desc) {
807         global $DB;
809         $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
811         $this->runid = $rec->runid;
812         $this->url = $rec->url;
813         $this->totalexecutiontime = $rec->totalexecutiontime;
814         $this->totalcputime = $rec->totalcputime;
815         $this->totalcalls = $rec->totalcalls;
816         $this->totalmemory = $rec->totalmemory;
817         $this->timecreated = $rec->timecreated;
819         $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
821         return unserialize(base64_decode($rec->data));
822     }
824     /**
825      * Given some run data, one type and, optionally, one runid
826      * store the information in DB
827      *
828      * Note that $type is completely ignored
829      */
830     public function save_run($xhprof_data, $type, $run_id = null) {
831         global $DB;
833         if (is_null($this->url)) {
834             xhprof_error("Warning: You must use the prepare_run() method before saving it");
835         }
837         // Calculate runid if needed
838         $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
840         // Calculate totals
841         $this->totalexecutiontime = $xhprof_data['main()']['wt'];
842         $this->totalcputime = $xhprof_data['main()']['cpu'];
843         $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
844         $this->totalmemory = $xhprof_data['main()']['mu'];
846         // Prepare data
847         $rec = new stdClass();
848         $rec->runid = $this->runid;
849         $rec->url = $this->url;
850         $rec->data = base64_encode(serialize($xhprof_data));
851         $rec->totalexecutiontime = $this->totalexecutiontime;
852         $rec->totalcputime = $this->totalcputime;
853         $rec->totalcalls = $this->totalcalls;
854         $rec->totalmemory = $this->totalmemory;
855         $rec->timecreated = $this->timecreated;
857         $DB->insert_record('profiling', $rec);
858         return $this->runid;
859     }
861     public function prepare_run($url) {
862         $this->url = $url;
863     }
865     // Private API starts here
867     protected function sum_calls($sum, $data) {
868         return $sum + $data['ct'];
869     }
872 /**
873  * Simple subclass of {@link table_sql} that provides
874  * some custom formatters for various columns, in order
875  * to make the main profiles list nicer
876  */
877 class xhprof_table_sql extends table_sql {
879     protected $listurlmode = false;
881     /**
882      * Get row classes to be applied based on row contents
883      */
884     function get_row_class($row) {
885         return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
886     }
888     /**
889      * Define it the table is in listurlmode or not, output will
890      * be different based on that
891      */
892     function set_listurlmode($listurlmode) {
893         $this->listurlmode = $listurlmode;
894     }
896     /**
897      * Format URL, so it points to last run for that url
898      */
899     protected function col_url($row) {
900         global $OUTPUT;
902         // Build the link to latest run for the script
903         $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
904         $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
906         // Decide, based on $this->listurlmode which actions to show
907         if ($this->listurlmode) {
908             $detailsaction = '';
909         } else {
910             // Build link icon to script details (pix + url + actionlink)
911             $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
912             $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
913             $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
914         }
916         return $scriptaction . '&nbsp;' . $detailsaction;
917     }
919     /**
920      * Format profiling date, human and pointing to run
921      */
922     protected function col_timecreated($row) {
923         global $OUTPUT;
924         $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
925         $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
926         return $OUTPUT->action_link($url, $fdate);
927     }
929     /**
930      * Format execution time
931      */
932     protected function col_totalexecutiontime($row) {
933         return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
934     }
936     /**
937      * Format cpu time
938      */
939     protected function col_totalcputime($row) {
940         return format_float($row->totalcputime / 1000, 3) . ' ms';
941     }
943     /**
944      * Format memory
945      */
946     protected function col_totalmemory($row) {
947         return format_float($row->totalmemory / 1024, 3) . ' KB';
948     }