2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
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.
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;
54 * Returns if profiling has been saved, optionally setting it
56 function profiling_is_saved($value = null) {
59 if (!is_null($value)) {
60 $saved = (bool)$value;
67 * Whether PHP profiling is available.
69 * This check ensures that one of the available PHP Profiling extensions is available.
73 function profiling_available() {
74 $hasextension = extension_loaded('tideways_xhprof');
75 $hasextension = $hasextension ||
extension_loaded('tideways');
76 $hasextension = $hasextension ||
extension_loaded('xhprof');
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()) {
92 // If profiling isn't enabled, nothing to start
93 if (empty($CFG->profilingenabled
) && empty($CFG->earlyprofilingenabled
)) {
97 // If profiling is already running or saved, nothing to start
98 if (profiling_is_running() ||
profiling_is_saved()) {
102 // Set script (from global if available, else our own)
103 $script = !empty($SCRIPT) ?
$SCRIPT : profiling_get_script();
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) {
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
)) {
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
) {
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) {
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));
176 xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY
, array('ignored_functions' => $ignore));
178 profiling_is_running(true);
180 // Started, return true
185 * Check for profiling flags in all possible places
186 * @param string $flag name
189 function profiling_get_flag($flag) {
190 return !empty(getenv($flag)) ||
191 isset($_COOKIE[$flag]) ||
192 isset($_POST[$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()) {
207 // If profiling isn't enabled, nothing to stop
208 if (empty($CFG->profilingenabled
) && empty($CFG->earlyprofilingenabled
)) {
212 // If profiling is not running or is already saved, nothing to stop
213 if (!profiling_is_running() ||
profiling_is_saved()) {
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();
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)) {
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.
246 $sql = "SELECT max(totalexecutiontime)
249 $slowest = $DB->get_field_sql($sql, array($script));
250 if (!empty($slowest) && $duration * 1000000 < $slowest) {
251 // Already have a worse profile stored.
256 $run = new moodle_xhprofrun();
257 $run->prepare_run($script);
258 $runid = $run->save_run($data, null);
259 profiling_is_saved(true);
262 profiling_prune_old_runs($runid);
264 // Finished, return true
268 function profiling_prune_old_runs($exception = 0) {
271 // Setting to 0 = no prune
272 if (empty($CFG->profilinglifetime
)) {
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() {
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);
312 function profiling_urls($report, $runid, $runid2 = null) {
318 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
321 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&run2=' . $runid2;
324 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
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;
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/> ".
349 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/> ".
350 "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
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
. '&runid2=' . $reference->runid
. '&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
. '&listurl=' . urlencode($run->url
);
397 $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
402 function profiling_print_rundiff($run1, $run2) {
403 global $CFG, $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');
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) {
460 $output = '<p class="centerpara buttons">';
461 $output .= ' <a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
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)) {
480 if (preg_match('~^' . $pattern . '$~', $string)) {
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;
494 if ($number1 != $number2) {
495 $perdiff = $number1 != 0 ?
($number2 * 100 / $number1) - 100 : 0;
497 $sign = $number2 > $number1 ?
'+' : '';
498 $delta = abs($perdiff) > 0.25 ?
'Δ' : '≈';
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
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) {
534 // Verify we have passed proper runids.
535 if (empty($runids)) {
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)) {
546 // Verify the $file path is writeable.
547 $base = dirname($file);
548 if (!is_writable($base)) {
552 // Create temp directory where the temp information will be generated.
553 $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
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.
561 $status = profiling_export_package($file, $tmpdir);
564 // Process finished ok, clean and return.
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 = '') {
582 // Any problem with the file or its directory, abort.
583 if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
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.
594 $mfile = $tmpdir . '/moodle_profiling_runs.xml';
595 if (!file_exists($mfile) or !is_readable($mfile)) {
598 $mdom = new DOMDocument();
599 if (!$mdom->load($mfile)) {
602 $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
607 // Verify all detail files exist and verify their format.
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)) {
615 $rdom = new DOMDocument();
616 if (!$rdom->load($rfile)) {
619 $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
625 // Everything looks ok, let's import all the runs.
628 foreach ($runs as $run) {
629 $rfile = $tmpdir . '/' . $run->getAttribute('ref');
630 $rdom = new DOMDocument();
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);
655 // Clean the temp directory used for import.
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) {
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();
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);
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);
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
);
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);
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');
732 $mainxw->end_tag('runs');
733 $mainxw->end_tag('moodle_profiling_runs');
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);
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);
766 * Return the xml schema for the main import file.
771 function profiling_get_import_main_schema() {
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">
778 <xs:element ref="info"/>
779 <xs:element ref="runs"/>
783 <xs:element name="info">
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"/>
794 <xs:element name="runs">
797 <xs:element maxOccurs="unbounded" ref="run"/>
801 <xs:element name="run">
803 <xs:attribute type="xs:int" name="id"/>
804 <xs:attribute type="xs:string" name="ref"/>
813 * Return the xml schema for each individual run import file.
818 function profiling_get_import_run_schema() {
820 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
821 <xs:element name="moodle_profiling_run">
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"/>
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) {
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
));
894 $info = unserialize(gzuncompress(base64_decode($rec->data
)));
895 if (!$this->reducedata
) {
896 // We want to return the full data.
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) {
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;
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'];
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);
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");
973 public function prepare_run($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.
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.
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, '==>')) {
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;
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, '==>')) {
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.
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 {
1090 foreach ($remaining as $key => $value) {
1091 // If we already have visited this element, we can skip it.
1092 if (isset($visited[$key])) {
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;
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;
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.
1115 $remaining = $newremaining; // There is still work to do.
1117 } while (count($remaining) > 0);
1119 // We are done, let's return the sorted array.
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) {
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 = '';
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 . ' ' . $detailsaction;
1172 * Format profiling date, human and pointing to run
1174 protected function col_timecreated($row) {
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';
1191 protected function col_totalcputime($row) {
1192 return format_float($row->totalcputime
/ 1000, 3) . ' ms';
1198 protected function col_totalmemory($row) {
1199 return format_float($row->totalmemory
/ 1024, 3) . ' KB';