Merge branch 'install_27_STABLE' of https://git.in.moodle.com/amosbot/moodle-install...
[moodle.git] / lib / xhprof / xhprof_moodle.php
blobb2f16fc2502541b326c91ec84311e82338385d80
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.
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
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
42 function profiling_is_running($value = null) {
43 static $running = null;
45 if (!is_null($value)) {
46 $running = (bool)$value;
49 return $running;
52 /**
53 * Returns if profiling has been saved, optionally setting it
55 function profiling_is_saved($value = null) {
56 static $saved = null;
58 if (!is_null($value)) {
59 $saved = (bool)$value;
62 return $saved;
65 /**
66 * Start profiling observing all the configuration
68 function profiling_start() {
69 global $CFG, $SESSION, $SCRIPT;
71 // If profiling isn't available, nothing to start
72 if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
73 return false;
76 // If profiling isn't enabled, nothing to start
77 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
78 return false;
81 // If profiling is already running or saved, nothing to start
82 if (profiling_is_running() || profiling_is_saved()) {
83 return false;
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;
108 // PROFILEALLSTOP detected, clean the mark in seesion and continue
109 if ($profileallstop && !empty($SESSION)) {
110 unset($SESSION->profileall);
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;
122 // Evaluate automatic (random) profiling if necessary
123 $profileauto = false;
124 if (!empty($CFG->profilingautofrec)) {
125 $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
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;
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 xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
150 profiling_is_running(true);
152 // Started, return true
153 return true;
157 * Stop profiling, gathering results and storing them
159 function profiling_stop() {
160 global $CFG, $DB, $SCRIPT;
162 // If profiling isn't available, nothing to stop
163 if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
164 return false;
167 // If profiling isn't enabled, nothing to stop
168 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
169 return false;
172 // If profiling is not running or is already saved, nothing to stop
173 if (!profiling_is_running() || profiling_is_saved()) {
174 return false;
177 // Set script (from global if available, else our own)
178 $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
180 // Arrived here, profiling is running, stop and save everything
181 profiling_is_running(false);
182 $data = xhprof_disable();
184 // We only save the run after ensuring the DB table exists
185 // (this prevents problems with profiling runs enabled in
186 // config.php before Moodle is installed. Rare but...
187 $tables = $DB->get_tables();
188 if (!in_array('profiling', $tables)) {
189 return false;
192 $run = new moodle_xhprofrun();
193 $run->prepare_run($script);
194 $runid = $run->save_run($data, null);
195 profiling_is_saved(true);
197 // Prune old runs
198 profiling_prune_old_runs($runid);
200 // Finished, return true
201 return true;
204 function profiling_prune_old_runs($exception = 0) {
205 global $CFG, $DB;
207 // Setting to 0 = no prune
208 if (empty($CFG->profilinglifetime)) {
209 return;
212 $cuttime = time() - ($CFG->profilinglifetime * 60);
213 $params = array('cuttime' => $cuttime, 'exception' => $exception);
215 $DB->delete_records_select('profiling', 'runreference = 0 AND
216 timecreated < :cuttime AND
217 runid != :exception', $params);
221 * Returns the path to the php script being requested
223 * Note this function is a partial copy of initialise_fullme() and
224 * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
225 * friends. To be used by early profiling runs in situations where
226 * $SCRIPT isn't defined yet
228 * @return string absolute path (wwwroot based) of the script being executed
230 function profiling_get_script() {
231 global $CFG;
233 $wwwroot = parse_url($CFG->wwwroot);
235 if (!isset($wwwroot['path'])) {
236 $wwwroot['path'] = '';
238 $wwwroot['path'] .= '/';
240 $path = $_SERVER['SCRIPT_NAME'];
242 if (strpos($path, $wwwroot['path']) === 0) {
243 return substr($path, strlen($wwwroot['path']) - 1);
245 return '';
248 function profiling_urls($report, $runid, $runid2 = null) {
249 global $CFG;
251 $url = '';
252 switch ($report) {
253 case 'run':
254 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
255 break;
256 case 'diff':
257 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
258 break;
259 case 'graph':
260 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
261 break;
263 return $url;
267 * Generate the output to print a profiling run including further actions you can then take.
269 * @param object $run The profiling run object we are going to display.
270 * @param array $prevreferences A list of run objects to list as comparison targets.
271 * @return string The output to display on the screen for this run.
273 function profiling_print_run($run, $prevreferences = null) {
274 global $CFG, $OUTPUT;
276 $output = '';
278 // Prepare the runreference/runcomment form
279 $checked = $run->runreference ? ' checked=checked' : '';
280 $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
281 "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
282 "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
283 "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
284 "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
285 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
286 "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
287 "</form>";
289 $table = new html_table();
290 $table->align = array('right', 'left');
291 $table->tablealign = 'center';
292 $table->attributes['class'] = 'profilingruntable';
293 $table->colclasses = array('label', 'value');
294 $table->data = array(
295 array(get_string('runid', 'tool_profiling'), $run->runid),
296 array(get_string('url'), $run->url),
297 array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
298 array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
299 array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
300 array(get_string('calls', 'tool_profiling'), $run->totalcalls),
301 array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
302 array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
303 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
304 // Add link to details
305 $strviewdetails = get_string('viewdetails', 'tool_profiling');
306 $url = profiling_urls('run', $run->runid);
307 $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
308 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
310 // If there are previous run(s) marked as reference, add link to diff.
311 if ($prevreferences) {
312 $table = new html_table();
313 $table->align = array('left', 'left');
314 $table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
315 $table->tablealign = 'center';
316 $table->attributes['class'] = 'flexible generaltable generalbox';
317 $table->colclasses = array('value', 'value', 'value');
318 $table->data = array();
320 $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');
322 foreach ($prevreferences as $reference) {
323 $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $reference->runid . '&amp;listurl=' . urlencode($run->url);
324 $row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);
325 $table->data[] = $row;
327 $output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');
330 // Add link to export this run.
331 $strexport = get_string('exportthis', 'tool_profiling');
332 $url = 'export.php?runid=' . $run->runid . '&amp;listurl=' . urlencode($run->url);
333 $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
335 return $output;
338 function profiling_print_rundiff($run1, $run2) {
339 global $CFG, $OUTPUT;
341 $output = '';
343 // Prepare the reference/comment information
344 $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
345 ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
346 $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
347 ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
349 // Calculate global differences
350 $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
351 $diffcputime = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
352 $diffcalls = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
353 $diffmemory = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
355 $table = new html_table();
356 $table->align = array('right', 'left', 'left', 'left');
357 $table->tablealign = 'center';
358 $table->attributes['class'] = 'profilingruntable';
359 $table->colclasses = array('label', 'value1', 'value2');
360 $table->data = array(
361 array(get_string('runid', 'tool_profiling'),
362 '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
363 '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
364 array(get_string('url'), $run1->url, $run2->url),
365 array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
366 userdate($run2->timecreated, '%d %B %Y, %H:%M')),
367 array(get_string('executiontime', 'tool_profiling'),
368 format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
369 format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
370 array(get_string('cputime', 'tool_profiling'),
371 format_float($run1->totalcputime / 1000, 3) . ' ms',
372 format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
373 array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
374 array(get_string('memory', 'tool_profiling'),
375 format_float($run1->totalmemory / 1024, 0) . ' KB',
376 format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
377 array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
378 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
379 // Add link to details
380 $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
381 $url = profiling_urls('diff', $run1->runid, $run2->runid);
382 //$url = $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
383 $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
384 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
385 return $output;
389 * Helper function that returns the HTML fragment to
390 * be displayed on listing mode, it includes actions
391 * like deletion/export/import...
393 function profiling_list_controls($listurl) {
394 global $CFG;
396 $output = '<p class="centerpara buttons">';
397 $output .= '&nbsp;<a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
398 $output .= '</p>';
400 return $output;
404 * Helper function that looks for matchings of one string
405 * against an array of * wildchar patterns
407 function profiling_string_matches($string, $patterns) {
408 $patterns = explode(',', $patterns);
409 foreach ($patterns as $pattern) {
410 // Trim and prepare pattern
411 $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
412 // Don't process empty patterns
413 if (empty($pattern)) {
414 continue;
416 if (preg_match('~' . $pattern . '~', $string)) {
417 return true;
420 return false;
424 * Helper function that, given to floats, returns their numerical
425 * and percentual differences, propertly formated and cssstyled
427 function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
428 $numdiff = $number2 - $number1;
429 $perdiff = 0;
430 if ($number1 != $number2) {
431 $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
433 $sign = $number2 > $number1 ? '+' : '';
434 $delta = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
435 $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
436 $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
437 $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
438 $endspan = '</span>';
439 $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
440 $fperdiff = $sign . format_float($perdiff, $numdec);
441 return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
445 * Export profiling runs to a .mpr (moodle profile runs) file.
447 * This function gets an array of profiling runs (array of runids) and
448 * saves a .mpr file into destination for ulterior handling.
450 * Format of .mpr files:
451 * mpr files are simple zip packages containing these files:
452 * - moodle_profiling_runs.xml: Metadata about the information
453 * exported. Contains some header information (version and
454 * release of moodle, database, git hash - if available, date
455 * of export...) and a list of all the runids included in the
456 * export.
457 * - runid.xml: One file per each run detailed in the main file,
458 * containing the raw dump of the given runid in the profiling table.
460 * Possible improvement: Start storing some extra information in the
461 * profiling table for each run (moodle version, database, git hash...).
463 * @param array $runids list of runids to be exported.
464 * @param string $file filesystem fullpath to destination .mpr file.
465 * @return boolean the mpr file has been successfully exported (true) or no (false).
467 function profiling_export_runs(array $runids, $file) {
468 global $CFG, $DB;
470 // Verify we have passed proper runids.
471 if (empty($runids)) {
472 return false;
475 // Verify all the passed runids do exist.
476 list ($insql, $inparams) = $DB->get_in_or_equal($runids);
477 $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
478 if ($reccount != count($runids)) {
479 return false;
482 // Verify the $file path is writeable.
483 $base = dirname($file);
484 if (!is_writable($base)) {
485 return false;
488 // Create temp directory where the temp information will be generated.
489 $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
490 mkdir($tmpdir);
492 // Generate the xml contents in the temp directory.
493 $status = profiling_export_generate($runids, $tmpdir);
495 // Package (zip) all the information into the final .mpr file.
496 if ($status) {
497 $status = profiling_export_package($file, $tmpdir);
500 // Process finished ok, clean and return.
501 fulldelete($tmpdir);
502 return $status;
506 * Import a .mpr (moodle profile runs) file into moodle.
508 * See {@link profiling_export_runs()} for more details about the
509 * implementation of .mpr files.
511 * @param string $file filesystem fullpath to target .mpr file.
512 * @param string $commentprefix prefix to add to the comments of all the imported runs.
513 * @return boolean the mpr file has been successfully imported (true) or no (false).
515 function profiling_import_runs($file, $commentprefix = '') {
516 global $DB;
518 // Any problem with the file or its directory, abort.
519 if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
520 return false;
523 // Unzip the file into temp directory.
524 $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
525 $fp = get_file_packer('application/vnd.moodle.profiling');
526 $status = $fp->extract_to_pathname($file, $tmpdir);
528 // Look for master file and verify its format.
529 if ($status) {
530 $mfile = $tmpdir . '/moodle_profiling_runs.xml';
531 if (!file_exists($mfile) or !is_readable($mfile)) {
532 $status = false;
533 } else {
534 $mdom = new DOMDocument();
535 if (!$mdom->load($mfile)) {
536 $status = false;
537 } else {
538 $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
543 // Verify all detail files exist and verify their format.
544 if ($status) {
545 $runs = $mdom->getElementsByTagName('run');
546 foreach ($runs as $run) {
547 $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);
548 if (!file_exists($rfile) or !is_readable($rfile)) {
549 $status = false;
550 } else {
551 $rdom = new DOMDocument();
552 if (!$rdom->load($rfile)) {
553 $status = false;
554 } else {
555 $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
561 // Everything looks ok, let's import all the runs.
562 if ($status) {
563 reset($runs);
564 foreach ($runs as $run) {
565 $rfile = $tmpdir . '/' . $run->getAttribute('ref');
566 $rdom = new DOMDocument();
567 $rdom->load($rfile);
568 $runarr = array();
569 $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);
570 $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);
571 $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);
572 $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);
573 $runarr['timecreated'] = time(); // Now.
574 $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);
575 $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);
576 $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);
577 $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);
578 $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);
579 // If the runid does not exist, insert it.
580 if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
581 $DB->insert_record('profiling', $runarr);
582 } else {
583 return false;
588 // Clean the temp directory used for import.
589 remove_dir($tmpdir);
591 return $status;
595 * Generate the mpr contents (xml files) in the temporal directory.
597 * @param array $runids list of runids to be generated.
598 * @param string $tmpdir filesystem fullpath of tmp generation.
599 * @return boolean the mpr contents have been generated (true) or no (false).
601 function profiling_export_generate(array $runids, $tmpdir) {
602 global $CFG, $DB;
604 // Calculate the header information to be sent to moodle_profiling_runs.xml.
605 $release = $CFG->release;
606 $version = $CFG->version;
607 $dbtype = $CFG->dbtype;
608 $githash = phpunit_util::get_git_hash();
609 $date = time();
611 // Create the xml output and writer for the main file.
612 $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
613 $mainxw = new xml_writer($mainxo);
615 // Output begins.
616 $mainxw->start();
617 $mainxw->begin_tag('moodle_profiling_runs');
619 // Send header information.
620 $mainxw->begin_tag('info');
621 $mainxw->full_tag('release', $release);
622 $mainxw->full_tag('version', $version);
623 $mainxw->full_tag('dbtype', $dbtype);
624 if ($githash) {
625 $mainxw->full_tag('githash', $githash);
627 $mainxw->full_tag('date', $date);
628 $mainxw->end_tag('info');
630 // Send information about runs.
631 $mainxw->begin_tag('runs');
632 foreach ($runids as $runid) {
633 // Get the run information from DB.
634 $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);
635 $attributes = array(
636 'id' => $run->id,
637 'ref' => $run->runid . '.xml');
638 $mainxw->full_tag('run', null, $attributes);
639 // Create the individual run file.
640 $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
641 $runxw = new xml_writer($runxo);
642 $runxw->start();
643 $runxw->begin_tag('moodle_profiling_run');
644 $runxw->full_tag('id', $run->id);
645 $runxw->full_tag('runid', $run->runid);
646 $runxw->full_tag('url', $run->url);
647 $runxw->full_tag('runreference', $run->runreference);
648 $runxw->full_tag('runcomment', $run->runcomment);
649 $runxw->full_tag('timecreated', $run->timecreated);
650 $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);
651 $runxw->full_tag('totalcputime', $run->totalcputime);
652 $runxw->full_tag('totalcalls', $run->totalcalls);
653 $runxw->full_tag('totalmemory', $run->totalmemory);
654 $runxw->full_tag('data', $run->data);
655 $runxw->end_tag('moodle_profiling_run');
656 $runxw->stop();
658 $mainxw->end_tag('runs');
659 $mainxw->end_tag('moodle_profiling_runs');
660 $mainxw->stop();
662 return true;
666 * Package (zip) the mpr contents (xml files) in the final location.
668 * @param string $file filesystem fullpath to destination .mpr file.
669 * @param string $tmpdir filesystem fullpath of tmp generation.
670 * @return boolean the mpr contents have been generated (true) or no (false).
672 function profiling_export_package($file, $tmpdir) {
673 // Get the list of files in $tmpdir.
674 $filestemp = get_directory_list($tmpdir, '', false, true, true);
675 $files = array();
677 // Add zip paths and fs paths to all them.
678 foreach ($filestemp as $filetemp) {
679 $files[$filetemp] = $tmpdir . '/' . $filetemp;
682 // Get the zip_packer.
683 $zippacker = get_file_packer('application/zip');
685 // Generate the packaged file.
686 $zippacker->archive_to_pathname($files, $file);
688 return true;
692 * Return the xml schema for the main import file.
694 * @return string
697 function profiling_get_import_main_schema() {
698 $schema = <<<EOS
699 <?xml version="1.0" encoding="UTF-8"?>
700 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
701 <xs:element name="moodle_profiling_runs">
702 <xs:complexType>
703 <xs:sequence>
704 <xs:element ref="info"/>
705 <xs:element ref="runs"/>
706 </xs:sequence>
707 </xs:complexType>
708 </xs:element>
709 <xs:element name="info">
710 <xs:complexType>
711 <xs:sequence>
712 <xs:element type="xs:string" name="release"/>
713 <xs:element type="xs:decimal" name="version"/>
714 <xs:element type="xs:string" name="dbtype"/>
715 <xs:element type="xs:string" minOccurs="0" name="githash"/>
716 <xs:element type="xs:int" name="date"/>
717 </xs:sequence>
718 </xs:complexType>
719 </xs:element>
720 <xs:element name="runs">
721 <xs:complexType>
722 <xs:sequence>
723 <xs:element maxOccurs="unbounded" ref="run"/>
724 </xs:sequence>
725 </xs:complexType>
726 </xs:element>
727 <xs:element name="run">
728 <xs:complexType>
729 <xs:attribute type="xs:int" name="id"/>
730 <xs:attribute type="xs:string" name="ref"/>
731 </xs:complexType>
732 </xs:element>
733 </xs:schema>
734 EOS;
735 return $schema;
739 * Return the xml schema for each individual run import file.
741 * @return string
744 function profiling_get_import_run_schema() {
745 $schema = <<<EOS
746 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
747 <xs:element name="moodle_profiling_run">
748 <xs:complexType>
749 <xs:sequence>
750 <xs:element type="xs:int" name="id"/>
751 <xs:element type="xs:string" name="runid"/>
752 <xs:element type="xs:string" name="url"/>
753 <xs:element type="xs:int" name="runreference"/>
754 <xs:element type="xs:string" name="runcomment"/>
755 <xs:element type="xs:int" name="timecreated"/>
756 <xs:element type="xs:int" name="totalexecutiontime"/>
757 <xs:element type="xs:int" name="totalcputime"/>
758 <xs:element type="xs:int" name="totalcalls"/>
759 <xs:element type="xs:int" name="totalmemory"/>
760 <xs:element type="xs:string" name="data"/>
761 </xs:sequence>
762 </xs:complexType>
763 </xs:element>
764 </xs:schema>
765 EOS;
766 return $schema;
769 * Custom implementation of iXHProfRuns
771 * This class is one implementation of the iXHProfRuns interface, in charge
772 * of storing and retrieve profiling run data to/from DB (profiling table)
774 * The interface only defines two methods to be defined: get_run() and
775 * save_run() we'll be implementing some more in order to keep all the
776 * rest of information in our runs properly handled.
778 class moodle_xhprofrun implements iXHProfRuns {
780 protected $runid = null;
781 protected $url = null;
782 protected $totalexecutiontime = 0;
783 protected $totalcputime = 0;
784 protected $totalcalls = 0;
785 protected $totalmemory = 0;
786 protected $timecreated = 0;
788 public function __construct() {
789 $this->timecreated = time();
793 * Given one runid and one type, return the run data
794 * and some extra info in run_desc from DB
796 * Note that $type is completely ignored
798 public function get_run($run_id, $type, &$run_desc) {
799 global $DB;
801 $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
803 $this->runid = $rec->runid;
804 $this->url = $rec->url;
805 $this->totalexecutiontime = $rec->totalexecutiontime;
806 $this->totalcputime = $rec->totalcputime;
807 $this->totalcalls = $rec->totalcalls;
808 $this->totalmemory = $rec->totalmemory;
809 $this->timecreated = $rec->timecreated;
811 $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
813 return unserialize(base64_decode($rec->data));
817 * Given some run data, one type and, optionally, one runid
818 * store the information in DB
820 * Note that $type is completely ignored
822 public function save_run($xhprof_data, $type, $run_id = null) {
823 global $DB;
825 if (is_null($this->url)) {
826 xhprof_error("Warning: You must use the prepare_run() method before saving it");
829 // Calculate runid if needed
830 $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
832 // Calculate totals
833 $this->totalexecutiontime = $xhprof_data['main()']['wt'];
834 $this->totalcputime = $xhprof_data['main()']['cpu'];
835 $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
836 $this->totalmemory = $xhprof_data['main()']['mu'];
838 // Prepare data
839 $rec = new stdClass();
840 $rec->runid = $this->runid;
841 $rec->url = $this->url;
842 $rec->data = base64_encode(serialize($xhprof_data));
843 $rec->totalexecutiontime = $this->totalexecutiontime;
844 $rec->totalcputime = $this->totalcputime;
845 $rec->totalcalls = $this->totalcalls;
846 $rec->totalmemory = $this->totalmemory;
847 $rec->timecreated = $this->timecreated;
849 $DB->insert_record('profiling', $rec);
850 return $this->runid;
853 public function prepare_run($url) {
854 $this->url = $url;
857 // Private API starts here
859 protected function sum_calls($sum, $data) {
860 return $sum + $data['ct'];
865 * Simple subclass of {@link table_sql} that provides
866 * some custom formatters for various columns, in order
867 * to make the main profiles list nicer
869 class xhprof_table_sql extends table_sql {
871 protected $listurlmode = false;
874 * Get row classes to be applied based on row contents
876 function get_row_class($row) {
877 return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
881 * Define it the table is in listurlmode or not, output will
882 * be different based on that
884 function set_listurlmode($listurlmode) {
885 $this->listurlmode = $listurlmode;
889 * Format URL, so it points to last run for that url
891 protected function col_url($row) {
892 global $OUTPUT;
894 // Build the link to latest run for the script
895 $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
896 $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
898 // Decide, based on $this->listurlmode which actions to show
899 if ($this->listurlmode) {
900 $detailsaction = '';
901 } else {
902 // Build link icon to script details (pix + url + actionlink)
903 $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
904 $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
905 $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
908 return $scriptaction . '&nbsp;' . $detailsaction;
912 * Format profiling date, human and pointing to run
914 protected function col_timecreated($row) {
915 global $OUTPUT;
916 $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
917 $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
918 return $OUTPUT->action_link($url, $fdate);
922 * Format execution time
924 protected function col_totalexecutiontime($row) {
925 return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
929 * Format cpu time
931 protected function col_totalcputime($row) {
932 return format_float($row->totalcputime / 1000, 3) . ' ms';
936 * Format memory
938 protected function col_totalmemory($row) {
939 return format_float($row->totalmemory / 1024, 3) . ' KB';