Merge branch 'MDL-79927' of https://github.com/paulholden/moodle
[moodle.git] / lib / xhprof / xhprof_moodle.php
blob86c63a6d526e52b0c88b9bf11cfe666596d62c8f
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 . '/filelib.php');
33 require_once($CFG->libdir . '/phpunit/classes/util.php');
34 require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
35 require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
36 require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');
38 // TODO: Change the implementation below to proper profiling class.
40 /**
41 * Returns if profiling is running, optionally setting it
43 function profiling_is_running($value = null) {
44 static $running = null;
46 if (!is_null($value)) {
47 $running = (bool)$value;
50 return $running;
53 /**
54 * Returns if profiling has been saved, optionally setting it
56 function profiling_is_saved($value = null) {
57 static $saved = null;
59 if (!is_null($value)) {
60 $saved = (bool)$value;
63 return $saved;
66 /**
67 * Whether PHP profiling is available.
69 * This check ensures that one of the available PHP Profiling extensions is available.
71 * @return bool
73 function profiling_available() {
74 $hasextension = extension_loaded('tideways_xhprof');
75 $hasextension = $hasextension || extension_loaded('tideways');
76 $hasextension = $hasextension || extension_loaded('xhprof');
78 return $hasextension;
81 /**
82 * Start profiling observing all the configuration
84 function profiling_start() {
85 global $CFG, $SESSION, $SCRIPT;
87 // If profiling isn't available, nothing to start
88 if (!profiling_available()) {
89 return false;
92 // If profiling isn't enabled, nothing to start
93 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
94 return false;
97 // If profiling is already running or saved, nothing to start
98 if (profiling_is_running() || profiling_is_saved()) {
99 return false;
102 // Set script (from global if available, else our own)
103 $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
105 // Get PGC variables
106 $profileme = profiling_get_flag('PROFILEME') && !empty($CFG->profilingallowme);
107 $dontprofileme = profiling_get_flag('DONTPROFILEME') && !empty($CFG->profilingallowme);
108 $profileall = profiling_get_flag('PROFILEALL') && !empty($CFG->profilingallowall);
109 $profileallstop = profiling_get_flag('PROFILEALLSTOP') && !empty($CFG->profilingallowall);
111 // DONTPROFILEME detected, nothing to start
112 if ($dontprofileme) {
113 return false;
116 // PROFILEALLSTOP detected, clean the mark in seesion and continue
117 if ($profileallstop && !empty($SESSION)) {
118 unset($SESSION->profileall);
121 // PROFILEALL detected, set the mark in session and continue
122 if ($profileall && !empty($SESSION)) {
123 $SESSION->profileall = true;
125 // SESSION->profileall detected, set $profileall
126 } else if (!empty($SESSION->profileall)) {
127 $profileall = true;
130 // Evaluate automatic (random) profiling if necessary
131 $profileauto = false;
132 if (!empty($CFG->profilingautofrec)) {
133 $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
136 // Profile potentially slow pages.
137 $profileslow = false;
138 if (!empty($CFG->profilingslow) && !CLI_SCRIPT) {
139 $profileslow = true;
142 // See if the $script matches any of the included patterns.
143 $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
144 $profileincluded = profiling_string_matches($script, $included);
146 // See if the $script matches any of the excluded patterns
147 $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
148 $profileexcluded = profiling_string_matches($script, $excluded);
150 // Decide if profile auto must happen (observe matchings)
151 $profileauto = $profileauto && $profileincluded && !$profileexcluded;
153 // Decide if profile by match must happen (only if profileauto is disabled)
154 $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);
156 // Decide if slow profile has been excluded.
157 $profileslow = $profileslow && !$profileexcluded;
159 // If not auto, me, all, match have been detected, nothing to do.
160 if (!$profileauto && !$profileme && !$profileall && !$profilematch && !$profileslow) {
161 return false;
164 // If we have only been triggered by a *potentially* slow page then remember this for later.
165 if ((!$profileauto && !$profileme && !$profileall && !$profilematch) && $profileslow) {
166 $CFG->profilepotentialslowpage = microtime(true); // Neither $PAGE or $SESSION are guaranteed here.
169 // Arrived here, the script is going to be profiled, let's do it
170 $ignore = array('call_user_func', 'call_user_func_array');
171 if (extension_loaded('tideways_xhprof')) {
172 tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY);
173 } else if (extension_loaded('tideways')) {
174 tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' => $ignore));
175 } else {
176 xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
178 profiling_is_running(true);
180 // Started, return true
181 return true;
185 * Check for profiling flags in all possible places
186 * @param string $flag name
187 * @return boolean
189 function profiling_get_flag($flag) {
190 return !empty(getenv($flag)) ||
191 isset($_COOKIE[$flag]) ||
192 isset($_POST[$flag]) ||
193 isset($_GET[$flag]);
197 * Stop profiling, gathering results and storing them
199 function profiling_stop() {
200 global $CFG, $DB, $SCRIPT;
202 // If profiling isn't available, nothing to stop
203 if (!profiling_available()) {
204 return false;
207 // If profiling isn't enabled, nothing to stop
208 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
209 return false;
212 // If profiling is not running or is already saved, nothing to stop
213 if (!profiling_is_running() || profiling_is_saved()) {
214 return false;
217 // Set script (from global if available, else our own)
218 $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
220 // Arrived here, profiling is running, stop and save everything
221 profiling_is_running(false);
222 if (extension_loaded('tideways_xhprof')) {
223 $data = tideways_xhprof_disable();
224 } else if (extension_loaded('tideways')) {
225 $data = tideways_disable();
226 } else {
227 $data = xhprof_disable();
230 // We only save the run after ensuring the DB table exists
231 // (this prevents problems with profiling runs enabled in
232 // config.php before Moodle is installed. Rare but...
233 $tables = $DB->get_tables();
234 if (!in_array('profiling', $tables)) {
235 return false;
238 // If we only profiled because it was potentially slow then...
239 if (!empty($CFG->profilepotentialslowpage)) {
240 $duration = microtime(true) - $CFG->profilepotentialslowpage;
241 if ($duration < $CFG->profilingslow) {
242 // Wasn't slow enough.
243 return false;
246 $sql = "SELECT max(totalexecutiontime)
247 FROM {profiling}
248 WHERE url = ?";
249 $slowest = $DB->get_field_sql($sql, array($script));
250 if (!empty($slowest) && $duration * 1000000 < $slowest) {
251 // Already have a worse profile stored.
252 return false;
256 $run = new moodle_xhprofrun();
257 $run->prepare_run($script);
258 $runid = $run->save_run($data, null);
259 profiling_is_saved(true);
261 // Prune old runs
262 profiling_prune_old_runs($runid);
264 // Finished, return true
265 return true;
268 function profiling_prune_old_runs($exception = 0) {
269 global $CFG, $DB;
271 // Setting to 0 = no prune
272 if (empty($CFG->profilinglifetime)) {
273 return;
276 $cuttime = time() - ($CFG->profilinglifetime * 60);
277 $params = array('cuttime' => $cuttime, 'exception' => $exception);
279 $DB->delete_records_select('profiling', 'runreference = 0 AND
280 timecreated < :cuttime AND
281 runid != :exception', $params);
285 * Returns the path to the php script being requested
287 * Note this function is a partial copy of initialise_fullme() and
288 * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
289 * friends. To be used by early profiling runs in situations where
290 * $SCRIPT isn't defined yet
292 * @return string absolute path (wwwroot based) of the script being executed
294 function profiling_get_script() {
295 global $CFG;
297 $wwwroot = parse_url($CFG->wwwroot);
299 if (!isset($wwwroot['path'])) {
300 $wwwroot['path'] = '';
302 $wwwroot['path'] .= '/';
304 $path = $_SERVER['SCRIPT_NAME'];
306 if (strpos($path, $wwwroot['path']) === 0) {
307 return substr($path, strlen($wwwroot['path']) - 1);
309 return '';
312 function profiling_urls($report, $runid, $runid2 = null) {
313 global $CFG;
315 $url = '';
316 switch ($report) {
317 case 'run':
318 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
319 break;
320 case 'diff':
321 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
322 break;
323 case 'graph':
324 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
325 break;
327 return $url;
331 * Generate the output to print a profiling run including further actions you can then take.
333 * @param object $run The profiling run object we are going to display.
334 * @param array $prevreferences A list of run objects to list as comparison targets.
335 * @return string The output to display on the screen for this run.
337 function profiling_print_run($run, $prevreferences = null) {
338 global $CFG, $OUTPUT;
340 $output = '';
342 // Prepare the runreference/runcomment form
343 $checked = $run->runreference ? ' checked=checked' : '';
344 $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
345 "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
346 "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
347 "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
348 "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
349 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
350 "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
351 "</form>";
353 $table = new html_table();
354 $table->align = array('right', 'left');
355 $table->tablealign = 'center';
356 $table->attributes['class'] = 'profilingruntable';
357 $table->colclasses = array('label', 'value');
358 $table->data = array(
359 array(get_string('runid', 'tool_profiling'), $run->runid),
360 array(get_string('url'), $run->url),
361 array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
362 array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
363 array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
364 array(get_string('calls', 'tool_profiling'), $run->totalcalls),
365 array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
366 array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
367 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
368 // Add link to details
369 $strviewdetails = get_string('viewdetails', 'tool_profiling');
370 $url = profiling_urls('run', $run->runid);
371 $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
372 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
374 // If there are previous run(s) marked as reference, add link to diff.
375 if ($prevreferences) {
376 $table = new html_table();
377 $table->align = array('left', 'left');
378 $table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
379 $table->tablealign = 'center';
380 $table->attributes['class'] = 'flexible generaltable generalbox';
381 $table->colclasses = array('value', 'value', 'value');
382 $table->data = array();
384 $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');
386 foreach ($prevreferences as $reference) {
387 $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $reference->runid . '&amp;listurl=' . urlencode($run->url);
388 $row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);
389 $table->data[] = $row;
391 $output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');
394 // Add link to export this run.
395 $strexport = get_string('exportthis', 'tool_profiling');
396 $url = 'export.php?runid=' . $run->runid . '&amp;listurl=' . urlencode($run->url);
397 $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
399 return $output;
402 function profiling_print_rundiff($run1, $run2) {
403 global $CFG, $OUTPUT;
405 $output = '';
407 // Prepare the reference/comment information
408 $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
409 ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
410 $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
411 ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
413 // Calculate global differences
414 $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
415 $diffcputime = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
416 $diffcalls = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
417 $diffmemory = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
419 $table = new html_table();
420 $table->align = array('right', 'left', 'left', 'left');
421 $table->tablealign = 'center';
422 $table->attributes['class'] = 'profilingruntable';
423 $table->colclasses = array('label', 'value1', 'value2');
424 $table->data = array(
425 array(get_string('runid', 'tool_profiling'),
426 '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
427 '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
428 array(get_string('url'), $run1->url, $run2->url),
429 array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
430 userdate($run2->timecreated, '%d %B %Y, %H:%M')),
431 array(get_string('executiontime', 'tool_profiling'),
432 format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
433 format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
434 array(get_string('cputime', 'tool_profiling'),
435 format_float($run1->totalcputime / 1000, 3) . ' ms',
436 format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
437 array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
438 array(get_string('memory', 'tool_profiling'),
439 format_float($run1->totalmemory / 1024, 0) . ' KB',
440 format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
441 array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
442 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
443 // Add link to details
444 $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
445 $url = profiling_urls('diff', $run1->runid, $run2->runid);
446 //$url = $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
447 $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
448 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
449 return $output;
453 * Helper function that returns the HTML fragment to
454 * be displayed on listing mode, it includes actions
455 * like deletion/export/import...
457 function profiling_list_controls($listurl) {
458 global $CFG;
460 $output = '<p class="centerpara buttons">';
461 $output .= '&nbsp;<a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
462 $output .= '</p>';
464 return $output;
468 * Helper function that looks for matchings of one string
469 * against an array of * wildchar patterns
471 function profiling_string_matches($string, $patterns) {
472 $patterns = preg_split("/\n|,/", $patterns);
473 foreach ($patterns as $pattern) {
474 // Trim and prepare pattern
475 $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
476 // Don't process empty patterns
477 if (empty($pattern)) {
478 continue;
480 if (preg_match('~^' . $pattern . '$~', $string)) {
481 return true;
484 return false;
488 * Helper function that, given to floats, returns their numerical
489 * and percentual differences, propertly formated and cssstyled
491 function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
492 $numdiff = $number2 - $number1;
493 $perdiff = 0;
494 if ($number1 != $number2) {
495 $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
497 $sign = $number2 > $number1 ? '+' : '';
498 $delta = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
499 $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
500 $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
501 $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
502 $endspan = '</span>';
503 $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
504 $fperdiff = $sign . format_float($perdiff, $numdec);
505 return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
509 * Export profiling runs to a .mpr (moodle profile runs) file.
511 * This function gets an array of profiling runs (array of runids) and
512 * saves a .mpr file into destination for ulterior handling.
514 * Format of .mpr files:
515 * mpr files are simple zip packages containing these files:
516 * - moodle_profiling_runs.xml: Metadata about the information
517 * exported. Contains some header information (version and
518 * release of moodle, database, git hash - if available, date
519 * of export...) and a list of all the runids included in the
520 * export.
521 * - runid.xml: One file per each run detailed in the main file,
522 * containing the raw dump of the given runid in the profiling table.
524 * Possible improvement: Start storing some extra information in the
525 * profiling table for each run (moodle version, database, git hash...).
527 * @param array $runids list of runids to be exported.
528 * @param string $file filesystem fullpath to destination .mpr file.
529 * @return boolean the mpr file has been successfully exported (true) or no (false).
531 function profiling_export_runs(array $runids, $file) {
532 global $CFG, $DB;
534 // Verify we have passed proper runids.
535 if (empty($runids)) {
536 return false;
539 // Verify all the passed runids do exist.
540 list ($insql, $inparams) = $DB->get_in_or_equal($runids);
541 $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
542 if ($reccount != count($runids)) {
543 return false;
546 // Verify the $file path is writeable.
547 $base = dirname($file);
548 if (!is_writable($base)) {
549 return false;
552 // Create temp directory where the temp information will be generated.
553 $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
554 mkdir($tmpdir);
556 // Generate the xml contents in the temp directory.
557 $status = profiling_export_generate($runids, $tmpdir);
559 // Package (zip) all the information into the final .mpr file.
560 if ($status) {
561 $status = profiling_export_package($file, $tmpdir);
564 // Process finished ok, clean and return.
565 fulldelete($tmpdir);
566 return $status;
570 * Import a .mpr (moodle profile runs) file into moodle.
572 * See {@link profiling_export_runs()} for more details about the
573 * implementation of .mpr files.
575 * @param string $file filesystem fullpath to target .mpr file.
576 * @param string $commentprefix prefix to add to the comments of all the imported runs.
577 * @return boolean the mpr file has been successfully imported (true) or no (false).
579 function profiling_import_runs($file, $commentprefix = '') {
580 global $DB;
582 // Any problem with the file or its directory, abort.
583 if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
584 return false;
587 // Unzip the file into temp directory.
588 $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
589 $fp = get_file_packer('application/vnd.moodle.profiling');
590 $status = $fp->extract_to_pathname($file, $tmpdir);
592 // Look for master file and verify its format.
593 if ($status) {
594 $mfile = $tmpdir . '/moodle_profiling_runs.xml';
595 if (!file_exists($mfile) or !is_readable($mfile)) {
596 $status = false;
597 } else {
598 $mdom = new DOMDocument();
599 if (!$mdom->load($mfile)) {
600 $status = false;
601 } else {
602 $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
607 // Verify all detail files exist and verify their format.
608 if ($status) {
609 $runs = $mdom->getElementsByTagName('run');
610 foreach ($runs as $run) {
611 $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);
612 if (!file_exists($rfile) or !is_readable($rfile)) {
613 $status = false;
614 } else {
615 $rdom = new DOMDocument();
616 if (!$rdom->load($rfile)) {
617 $status = false;
618 } else {
619 $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
625 // Everything looks ok, let's import all the runs.
626 if ($status) {
627 reset($runs);
628 foreach ($runs as $run) {
629 $rfile = $tmpdir . '/' . $run->getAttribute('ref');
630 $rdom = new DOMDocument();
631 $rdom->load($rfile);
632 $runarr = array();
633 $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);
634 $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);
635 $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);
636 $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);
637 $runarr['timecreated'] = time(); // Now.
638 $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);
639 $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);
640 $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);
641 $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);
642 $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);
643 // If the runid does not exist, insert it.
644 if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
645 if (@gzuncompress(base64_decode($runarr['data'])) === false) {
646 $runarr['data'] = base64_encode(gzcompress(base64_decode($runarr['data'])));
648 $DB->insert_record('profiling', $runarr);
649 } else {
650 return false;
655 // Clean the temp directory used for import.
656 remove_dir($tmpdir);
658 return $status;
662 * Generate the mpr contents (xml files) in the temporal directory.
664 * @param array $runids list of runids to be generated.
665 * @param string $tmpdir filesystem fullpath of tmp generation.
666 * @return boolean the mpr contents have been generated (true) or no (false).
668 function profiling_export_generate(array $runids, $tmpdir) {
669 global $CFG, $DB;
671 if (empty($CFG->release) || empty($CFG->version)) {
672 // Some scripts may not have included version.php.
673 include($CFG->dirroot.'/version.php');
674 $CFG->release = $release;
675 $CFG->version = $version;
678 // Calculate the header information to be sent to moodle_profiling_runs.xml.
679 $release = $CFG->release;
680 $version = $CFG->version;
681 $dbtype = $CFG->dbtype;
682 $githash = phpunit_util::get_git_hash();
683 $date = time();
685 // Create the xml output and writer for the main file.
686 $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
687 $mainxw = new xml_writer($mainxo);
689 // Output begins.
690 $mainxw->start();
691 $mainxw->begin_tag('moodle_profiling_runs');
693 // Send header information.
694 $mainxw->begin_tag('info');
695 $mainxw->full_tag('release', $release);
696 $mainxw->full_tag('version', $version);
697 $mainxw->full_tag('dbtype', $dbtype);
698 if ($githash) {
699 $mainxw->full_tag('githash', $githash);
701 $mainxw->full_tag('date', $date);
702 $mainxw->end_tag('info');
704 // Send information about runs.
705 $mainxw->begin_tag('runs');
706 foreach ($runids as $runid) {
707 // Get the run information from DB.
708 $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);
709 $attributes = array(
710 'id' => $run->id,
711 'ref' => $run->runid . '.xml');
712 $mainxw->full_tag('run', null, $attributes);
713 // Create the individual run file.
714 $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
715 $runxw = new xml_writer($runxo);
716 $runxw->start();
717 $runxw->begin_tag('moodle_profiling_run');
718 $runxw->full_tag('id', $run->id);
719 $runxw->full_tag('runid', $run->runid);
720 $runxw->full_tag('url', $run->url);
721 $runxw->full_tag('runreference', $run->runreference);
722 $runxw->full_tag('runcomment', $run->runcomment);
723 $runxw->full_tag('timecreated', $run->timecreated);
724 $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);
725 $runxw->full_tag('totalcputime', $run->totalcputime);
726 $runxw->full_tag('totalcalls', $run->totalcalls);
727 $runxw->full_tag('totalmemory', $run->totalmemory);
728 $runxw->full_tag('data', $run->data);
729 $runxw->end_tag('moodle_profiling_run');
730 $runxw->stop();
732 $mainxw->end_tag('runs');
733 $mainxw->end_tag('moodle_profiling_runs');
734 $mainxw->stop();
736 return true;
740 * Package (zip) the mpr contents (xml files) in the final location.
742 * @param string $file filesystem fullpath to destination .mpr file.
743 * @param string $tmpdir filesystem fullpath of tmp generation.
744 * @return boolean the mpr contents have been generated (true) or no (false).
746 function profiling_export_package($file, $tmpdir) {
747 // Get the list of files in $tmpdir.
748 $filestemp = get_directory_list($tmpdir, '', false, true, true);
749 $files = array();
751 // Add zip paths and fs paths to all them.
752 foreach ($filestemp as $filetemp) {
753 $files[$filetemp] = $tmpdir . '/' . $filetemp;
756 // Get the zip_packer.
757 $zippacker = get_file_packer('application/zip');
759 // Generate the packaged file.
760 $zippacker->archive_to_pathname($files, $file);
762 return true;
766 * Return the xml schema for the main import file.
768 * @return string
771 function profiling_get_import_main_schema() {
772 $schema = <<<EOS
773 <?xml version="1.0" encoding="UTF-8"?>
774 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
775 <xs:element name="moodle_profiling_runs">
776 <xs:complexType>
777 <xs:sequence>
778 <xs:element ref="info"/>
779 <xs:element ref="runs"/>
780 </xs:sequence>
781 </xs:complexType>
782 </xs:element>
783 <xs:element name="info">
784 <xs:complexType>
785 <xs:sequence>
786 <xs:element type="xs:string" name="release"/>
787 <xs:element type="xs:decimal" name="version"/>
788 <xs:element type="xs:string" name="dbtype"/>
789 <xs:element type="xs:string" minOccurs="0" name="githash"/>
790 <xs:element type="xs:int" name="date"/>
791 </xs:sequence>
792 </xs:complexType>
793 </xs:element>
794 <xs:element name="runs">
795 <xs:complexType>
796 <xs:sequence>
797 <xs:element maxOccurs="unbounded" ref="run"/>
798 </xs:sequence>
799 </xs:complexType>
800 </xs:element>
801 <xs:element name="run">
802 <xs:complexType>
803 <xs:attribute type="xs:int" name="id"/>
804 <xs:attribute type="xs:string" name="ref"/>
805 </xs:complexType>
806 </xs:element>
807 </xs:schema>
808 EOS;
809 return $schema;
813 * Return the xml schema for each individual run import file.
815 * @return string
818 function profiling_get_import_run_schema() {
819 $schema = <<<EOS
820 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
821 <xs:element name="moodle_profiling_run">
822 <xs:complexType>
823 <xs:sequence>
824 <xs:element type="xs:int" name="id"/>
825 <xs:element type="xs:string" name="runid"/>
826 <xs:element type="xs:string" name="url"/>
827 <xs:element type="xs:int" name="runreference"/>
828 <xs:element type="xs:string" name="runcomment"/>
829 <xs:element type="xs:int" name="timecreated"/>
830 <xs:element type="xs:integer" name="totalexecutiontime"/>
831 <xs:element type="xs:integer" name="totalcputime"/>
832 <xs:element type="xs:integer" name="totalcalls"/>
833 <xs:element type="xs:integer" name="totalmemory"/>
834 <xs:element type="xs:string" name="data"/>
835 </xs:sequence>
836 </xs:complexType>
837 </xs:element>
838 </xs:schema>
839 EOS;
840 return $schema;
843 * Custom implementation of iXHProfRuns
845 * This class is one implementation of the iXHProfRuns interface, in charge
846 * of storing and retrieve profiling run data to/from DB (profiling table)
848 * The interface only defines two methods to be defined: get_run() and
849 * save_run() we'll be implementing some more in order to keep all the
850 * rest of information in our runs properly handled.
852 class moodle_xhprofrun implements iXHProfRuns {
854 protected $runid = null;
855 protected $url = null;
856 protected $totalexecutiontime = 0;
857 protected $totalcputime = 0;
858 protected $totalcalls = 0;
859 protected $totalmemory = 0;
860 protected $timecreated = 0;
862 /** @var bool Decide if we want to reduce profiling data or no */
863 protected bool $reducedata = false;
865 public function __construct() {
866 $this->timecreated = time();
870 * Given one runid and one type, return the run data
871 * and some extra info in run_desc from DB
873 * Note that $type is completely ignored
875 public function get_run($run_id, $type, &$run_desc) {
876 global $DB;
878 $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
880 $this->runid = $rec->runid;
881 $this->url = $rec->url;
882 $this->totalexecutiontime = $rec->totalexecutiontime;
883 $this->totalcputime = $rec->totalcputime;
884 $this->totalcalls = $rec->totalcalls;
885 $this->totalmemory = $rec->totalmemory;
886 $this->timecreated = $rec->timecreated;
888 $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
890 // Handle historical runs that aren't compressed.
891 if (@gzuncompress(base64_decode($rec->data)) === false) {
892 return unserialize(base64_decode($rec->data));
893 } else {
894 $info = unserialize(gzuncompress(base64_decode($rec->data)));
895 if (!$this->reducedata) {
896 // We want to return the full data.
897 return $info;
900 // We want to apply some transformations here, in order to reduce
901 // the information for some complex (too many levels) cases.
902 return $this->reduce_run_data($info);
907 * Given some run data, one type and, optionally, one runid
908 * store the information in DB
910 * Note that $type is completely ignored
912 public function save_run($xhprof_data, $type, $run_id = null) {
913 global $DB, $CFG;
915 if (is_null($this->url)) {
916 xhprof_error("Warning: You must use the prepare_run() method before saving it");
919 // Calculate runid if needed
920 $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
922 // Calculate totals
923 $this->totalexecutiontime = $xhprof_data['main()']['wt'];
924 $this->totalcputime = $xhprof_data['main()']['cpu'];
925 $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
926 $this->totalmemory = $xhprof_data['main()']['mu'];
928 // Prepare data
929 $rec = new stdClass();
930 $rec->runid = $this->runid;
931 $rec->url = $this->url;
932 $rec->totalexecutiontime = $this->totalexecutiontime;
933 $rec->totalcputime = $this->totalcputime;
934 $rec->totalcalls = $this->totalcalls;
935 $rec->totalmemory = $this->totalmemory;
936 $rec->timecreated = $this->timecreated;
938 // Send to database with compressed and endoded data.
939 if (empty($CFG->disableprofilingtodatabase)) {
940 $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
941 $DB->insert_record('profiling', $rec);
944 // Send raw data to plugins.
945 $rec->data = $xhprof_data;
947 // Allow a plugin to take the trace data and process it.
948 if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
949 foreach ($pluginsfunction as $plugintype => $plugins) {
950 foreach ($plugins as $pluginfunction) {
951 $pluginfunction($rec);
956 if (PHPUNIT_TEST) {
957 // Calculate export variables.
958 $tempdir = 'profiling';
959 make_temp_directory($tempdir);
960 $runids = array($this->runid);
961 $filename = $this->runid . '.mpr';
962 $filepath = $CFG->tempdir . '/' . $tempdir . '/' . $filename;
964 // Generate the mpr file and send it.
965 if (profiling_export_runs($runids, $filepath)) {
966 fprintf(STDERR, "Profiling data saved to: ".$filepath."\n");
970 return $this->runid;
973 public function prepare_run($url) {
974 $this->url = $url;
978 * Enable or disable reducing profiling data.
980 * @param bool $reducedata Decide if we want to reduce profiling data (true) or no (false).
982 public function set_reducedata(bool $reducedata): void {
983 $this->reducedata = $reducedata;
986 // Private API starts here.
988 protected function sum_calls($sum, $data) {
989 return $sum + $data['ct'];
993 * Reduce the run data to a more manageable size.
995 * This removes from the run data all the entries that
996 * are matching a group of regular expressions.
998 * The main use is to remove all the calls between "__Mustache"
999 * functions, which don't provide any useful information and
1000 * make the call-graph too complex to be handled.
1002 * @param array $info The xhprof run data, original array.
1003 * @return array The xhprof run data, reduced array.
1005 protected function reduce_run_data(array $info): array {
1006 // Define which (regular expressions) we want to remove. Already escaped if needed to, please.
1007 $toremove = [
1008 '__Mustache.*==>__Mustache.*', // All __Mustache to __Mustache calls.
1010 // Build the regular expression to be used.
1011 $regexp = '/^(' . implode('|', $toremove) . ')$/';
1013 // Given that the keys of the array have the format "parent==>child"
1014 // we want to rebuild the array with the same structure but
1015 // topologically sorted (parents always before children).
1016 // Note that we do this exclusively to guarantee that the
1017 // second pass (see below) works properly in all cases because,
1018 // without it, we may need to perform N (while loop) second passes.
1019 $sorted = $this->xhprof_topo_sort($info);
1021 // To keep track of removed and remaining (child-parent) pairs.
1022 $removed = [];
1023 $remaining = [];
1025 // First pass, we are going to remove all the elements which
1026 // both parent and child are __Mustache function calls.
1027 foreach ($sorted as $key => $value) {
1028 if (!str_contains($key, '==>')) {
1029 $parent = 'NULL';
1030 $child = $key;
1031 } else {
1032 [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
1035 if (preg_match($regexp, $key)) {
1036 unset($sorted[$key]);
1037 $removed[$child][$parent] = true;
1038 } else {
1039 $remaining[$child][$parent] = true;
1043 // Second pass, we are going to remove all the elements which
1044 // parent was removed by first pass and doesn't appear anymore
1045 // as a child of anything (aka, they have become orphaned).
1046 // Note, that thanks to the topological sorting, we can be sure
1047 // one unique pass is enough. Without it, we may need to perform
1048 // N (while loop) second passes.
1049 foreach ($sorted as $key => $value) {
1050 if (!str_contains($key, '==>')) {
1051 $parent = 'NULL';
1052 $child = $key;
1053 } else {
1054 [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
1057 if (isset($removed[$parent]) && !isset($remaining[$parent])) {
1058 unset($sorted[$key]);
1059 $removed[$child][$parent] = true;
1060 unset($remaining[$child][$parent]);
1061 // If this was the last parent of this child, remove it completely from the remaining array.
1062 if (empty($remaining[$child])) {
1063 unset($remaining[$child]);
1068 // We are done, let's return the reduced array.
1069 return $sorted;
1074 * Sort the xhprof run pseudo-topologically, so all parents are always before their children.
1076 * Note that this is not a proper, complex, recursive topological sorting algorithm, returning
1077 * nodes that later have to be converted back to xhprof "pairs" but, instead, does the specific
1078 * work to get those parent==>child (2 levels only) "pairs" sorted (parents always before children).
1080 * @param array $info The xhprof run data, original array.
1082 * @return array The xhprof run data, sorted array.
1084 protected function xhprof_topo_sort(array $info): array {
1085 $sorted = [];
1086 $visited = [];
1087 $remaining = $info;
1088 do {
1089 $newremaining = [];
1090 foreach ($remaining as $key => $value) {
1091 // If we already have visited this element, we can skip it.
1092 if (isset($visited[$key])) {
1093 continue;
1095 if (!str_contains($key, '==>')) {
1096 // It's a root element, we can add it to the sorted array.
1097 $sorted[$key] = $info[$key];
1098 $visited[$key] = true;
1099 } else {
1100 [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
1101 if (isset($visited[$parent])) {
1102 // Parent already visited, we can add any children to the sorted array.
1103 $sorted[$key] = $info[$key];
1104 $visited[$child] = true;
1105 } else {
1106 // Cannot add this yet, we need to wait for the parent.
1107 $newremaining[$key] = $value;
1111 // Protection against infinite loops.
1112 if (count($remaining) === count($newremaining)) {
1113 $remaining = []; // So we exit the do...while loop.
1114 } else {
1115 $remaining = $newremaining; // There is still work to do.
1117 } while (count($remaining) > 0);
1119 // We are done, let's return the sorted array.
1120 return $sorted;
1125 * Simple subclass of {@link table_sql} that provides
1126 * some custom formatters for various columns, in order
1127 * to make the main profiles list nicer
1129 class xhprof_table_sql extends table_sql {
1131 protected $listurlmode = false;
1134 * Get row classes to be applied based on row contents
1136 function get_row_class($row) {
1137 return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
1141 * Define it the table is in listurlmode or not, output will
1142 * be different based on that
1144 function set_listurlmode($listurlmode) {
1145 $this->listurlmode = $listurlmode;
1149 * Format URL, so it points to last run for that url
1151 protected function col_url($row) {
1152 global $OUTPUT;
1154 // Build the link to latest run for the script
1155 $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
1156 $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
1158 // Decide, based on $this->listurlmode which actions to show
1159 if ($this->listurlmode) {
1160 $detailsaction = '';
1161 } else {
1162 // Build link icon to script details (pix + url + actionlink)
1163 $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
1164 $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
1165 $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
1168 return $scriptaction . '&nbsp;' . $detailsaction;
1172 * Format profiling date, human and pointing to run
1174 protected function col_timecreated($row) {
1175 global $OUTPUT;
1176 $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
1177 $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
1178 return $OUTPUT->action_link($url, $fdate);
1182 * Format execution time
1184 protected function col_totalexecutiontime($row) {
1185 return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
1189 * Format cpu time
1191 protected function col_totalcputime($row) {
1192 return format_float($row->totalcputime / 1000, 3) . ' ms';
1196 * Format memory
1198 protected function col_totalmemory($row) {
1199 return format_float($row->totalmemory / 1024, 3) . ' KB';