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 $check = 'PROFILEME';
107 $profileme = isset($_POST[$check]) ||
isset($_GET[$check]) ||
isset($_COOKIE[$check]) ?
true : false;
108 $profileme = $profileme && !empty($CFG->profilingallowme
);
109 $check = 'DONTPROFILEME';
110 $dontprofileme = isset($_POST[$check]) ||
isset($_GET[$check]) ||
isset($_COOKIE[$check]) ?
true : false;
111 $dontprofileme = $dontprofileme && !empty($CFG->profilingallowme
);
112 $check = 'PROFILEALL';
113 $profileall = isset($_POST[$check]) ||
isset($_GET[$check]) ||
isset($_COOKIE[$check]) ?
true : false;
114 $profileall = $profileall && !empty($CFG->profilingallowall
);
115 $check = 'PROFILEALLSTOP';
116 $profileallstop = isset($_POST[$check]) ||
isset($_GET[$check]) ||
isset($_COOKIE[$check]) ?
true : false;
117 $profileallstop = $profileallstop && !empty($CFG->profilingallowall
);
119 // DONTPROFILEME detected, nothing to start
120 if ($dontprofileme) {
124 // PROFILEALLSTOP detected, clean the mark in seesion and continue
125 if ($profileallstop && !empty($SESSION)) {
126 unset($SESSION->profileall
);
129 // PROFILEALL detected, set the mark in session and continue
130 if ($profileall && !empty($SESSION)) {
131 $SESSION->profileall
= true;
133 // SESSION->profileall detected, set $profileall
134 } else if (!empty($SESSION->profileall
)) {
138 // Evaluate automatic (random) profiling if necessary
139 $profileauto = false;
140 if (!empty($CFG->profilingautofrec
)) {
141 $profileauto = (mt_rand(1, $CFG->profilingautofrec
) === 1);
144 // See if the $script matches any of the included patterns
145 $included = empty($CFG->profilingincluded
) ?
'' : $CFG->profilingincluded
;
146 $profileincluded = profiling_string_matches($script, $included);
148 // See if the $script matches any of the excluded patterns
149 $excluded = empty($CFG->profilingexcluded
) ?
'' : $CFG->profilingexcluded
;
150 $profileexcluded = profiling_string_matches($script, $excluded);
152 // Decide if profile auto must happen (observe matchings)
153 $profileauto = $profileauto && $profileincluded && !$profileexcluded;
155 // Decide if profile by match must happen (only if profileauto is disabled)
156 $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec
);
158 // If not auto, me, all, match have been detected, nothing to do
159 if (!$profileauto && !$profileme && !$profileall && !$profilematch) {
163 // Arrived here, the script is going to be profiled, let's do it
164 $ignore = array('call_user_func', 'call_user_func_array');
165 if (extension_loaded('tideways_xhprof')) {
166 tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY
);
167 } else if (extension_loaded('tideways')) {
168 tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY
, array('ignored_functions' => $ignore));
170 xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY
, array('ignored_functions' => $ignore));
172 profiling_is_running(true);
174 // Started, return true
179 * Stop profiling, gathering results and storing them
181 function profiling_stop() {
182 global $CFG, $DB, $SCRIPT;
184 // If profiling isn't available, nothing to stop
185 if (!profiling_available()) {
189 // If profiling isn't enabled, nothing to stop
190 if (empty($CFG->profilingenabled
) && empty($CFG->earlyprofilingenabled
)) {
194 // If profiling is not running or is already saved, nothing to stop
195 if (!profiling_is_running() ||
profiling_is_saved()) {
199 // Set script (from global if available, else our own)
200 $script = !empty($SCRIPT) ?
$SCRIPT : profiling_get_script();
202 // Arrived here, profiling is running, stop and save everything
203 profiling_is_running(false);
204 if (extension_loaded('tideways_xhprof')) {
205 $data = tideways_xhprof_disable();
206 } else if (extension_loaded('tideways')) {
207 $data = tideways_disable();
209 $data = xhprof_disable();
212 // We only save the run after ensuring the DB table exists
213 // (this prevents problems with profiling runs enabled in
214 // config.php before Moodle is installed. Rare but...
215 $tables = $DB->get_tables();
216 if (!in_array('profiling', $tables)) {
220 $run = new moodle_xhprofrun();
221 $run->prepare_run($script);
222 $runid = $run->save_run($data, null);
223 profiling_is_saved(true);
226 profiling_prune_old_runs($runid);
228 // Finished, return true
232 function profiling_prune_old_runs($exception = 0) {
235 // Setting to 0 = no prune
236 if (empty($CFG->profilinglifetime
)) {
240 $cuttime = time() - ($CFG->profilinglifetime
* 60);
241 $params = array('cuttime' => $cuttime, 'exception' => $exception);
243 $DB->delete_records_select('profiling', 'runreference = 0 AND
244 timecreated < :cuttime AND
245 runid != :exception', $params);
249 * Returns the path to the php script being requested
251 * Note this function is a partial copy of initialise_fullme() and
252 * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
253 * friends. To be used by early profiling runs in situations where
254 * $SCRIPT isn't defined yet
256 * @return string absolute path (wwwroot based) of the script being executed
258 function profiling_get_script() {
261 $wwwroot = parse_url($CFG->wwwroot
);
263 if (!isset($wwwroot['path'])) {
264 $wwwroot['path'] = '';
266 $wwwroot['path'] .= '/';
268 $path = $_SERVER['SCRIPT_NAME'];
270 if (strpos($path, $wwwroot['path']) === 0) {
271 return substr($path, strlen($wwwroot['path']) - 1);
276 function profiling_urls($report, $runid, $runid2 = null) {
282 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
285 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&run2=' . $runid2;
288 $url = $CFG->wwwroot
. '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
295 * Generate the output to print a profiling run including further actions you can then take.
297 * @param object $run The profiling run object we are going to display.
298 * @param array $prevreferences A list of run objects to list as comparison targets.
299 * @return string The output to display on the screen for this run.
301 function profiling_print_run($run, $prevreferences = null) {
302 global $CFG, $OUTPUT;
306 // Prepare the runreference/runcomment form
307 $checked = $run->runreference ?
' checked=checked' : '';
308 $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
309 "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
310 "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
311 "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
312 "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/> ".
313 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/> ".
314 "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
317 $table = new html_table();
318 $table->align
= array('right', 'left');
319 $table->tablealign
= 'center';
320 $table->attributes
['class'] = 'profilingruntable';
321 $table->colclasses
= array('label', 'value');
322 $table->data
= array(
323 array(get_string('runid', 'tool_profiling'), $run->runid
),
324 array(get_string('url'), $run->url
),
325 array(get_string('date'), userdate($run->timecreated
, '%d %B %Y, %H:%M')),
326 array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime
/ 1000, 3) . ' ms'),
327 array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime
/ 1000, 3) . ' ms'),
328 array(get_string('calls', 'tool_profiling'), $run->totalcalls
),
329 array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory
/ 1024, 0) . ' KB'),
330 array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
331 $output = $OUTPUT->box(html_writer
::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
332 // Add link to details
333 $strviewdetails = get_string('viewdetails', 'tool_profiling');
334 $url = profiling_urls('run', $run->runid
);
335 $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
336 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
338 // If there are previous run(s) marked as reference, add link to diff.
339 if ($prevreferences) {
340 $table = new html_table();
341 $table->align
= array('left', 'left');
342 $table->head
= array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
343 $table->tablealign
= 'center';
344 $table->attributes
['class'] = 'flexible generaltable generalbox';
345 $table->colclasses
= array('value', 'value', 'value');
346 $table->data
= array();
348 $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');
350 foreach ($prevreferences as $reference) {
351 $url = 'index.php?runid=' . $run->runid
. '&runid2=' . $reference->runid
. '&listurl=' . urlencode($run->url
);
352 $row = array(userdate($reference->timecreated
), '<a href="' . $url . '" title="">'.$reference->runid
.'</a>', $reference->runcomment
);
353 $table->data
[] = $row;
355 $output .= $OUTPUT->box(html_writer
::table($table), 'profilingrunbox', 'profiling_diffs');
358 // Add link to export this run.
359 $strexport = get_string('exportthis', 'tool_profiling');
360 $url = 'export.php?runid=' . $run->runid
. '&listurl=' . urlencode($run->url
);
361 $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
366 function profiling_print_rundiff($run1, $run2) {
367 global $CFG, $OUTPUT;
371 // Prepare the reference/comment information
372 $referencetext1 = ($run1->runreference ?
get_string('yes') : get_string('no')) .
373 ($run1->runcomment ?
' - ' . s($run1->runcomment
) : '');
374 $referencetext2 = ($run2->runreference ?
get_string('yes') : get_string('no')) .
375 ($run2->runcomment ?
' - ' . s($run2->runcomment
) : '');
377 // Calculate global differences
378 $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime
, $run2->totalexecutiontime
, 'ms', 1000);
379 $diffcputime = profiling_get_difference($run1->totalcputime
, $run2->totalcputime
, 'ms', 1000);
380 $diffcalls = profiling_get_difference($run1->totalcalls
, $run2->totalcalls
);
381 $diffmemory = profiling_get_difference($run1->totalmemory
, $run2->totalmemory
, 'KB', 1024);
383 $table = new html_table();
384 $table->align
= array('right', 'left', 'left', 'left');
385 $table->tablealign
= 'center';
386 $table->attributes
['class'] = 'profilingruntable';
387 $table->colclasses
= array('label', 'value1', 'value2');
388 $table->data
= array(
389 array(get_string('runid', 'tool_profiling'),
390 '<a href="index.php?runid=' . $run1->runid
. '&listurl=' . urlencode($run1->url
) . '" title="">' . $run1->runid
. '</a>',
391 '<a href="index.php?runid=' . $run2->runid
. '&listurl=' . urlencode($run2->url
) . '" title="">' . $run2->runid
. '</a>'),
392 array(get_string('url'), $run1->url
, $run2->url
),
393 array(get_string('date'), userdate($run1->timecreated
, '%d %B %Y, %H:%M'),
394 userdate($run2->timecreated
, '%d %B %Y, %H:%M')),
395 array(get_string('executiontime', 'tool_profiling'),
396 format_float($run1->totalexecutiontime
/ 1000, 3) . ' ms',
397 format_float($run2->totalexecutiontime
/ 1000, 3) . ' ms ' . $diffexecutiontime),
398 array(get_string('cputime', 'tool_profiling'),
399 format_float($run1->totalcputime
/ 1000, 3) . ' ms',
400 format_float($run2->totalcputime
/ 1000, 3) . ' ms ' . $diffcputime),
401 array(get_string('calls', 'tool_profiling'), $run1->totalcalls
, $run2->totalcalls
. ' ' . $diffcalls),
402 array(get_string('memory', 'tool_profiling'),
403 format_float($run1->totalmemory
/ 1024, 0) . ' KB',
404 format_float($run2->totalmemory
/ 1024, 0) . ' KB ' . $diffmemory),
405 array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
406 $output = $OUTPUT->box(html_writer
::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
407 // Add link to details
408 $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
409 $url = profiling_urls('diff', $run1->runid
, $run2->runid
);
410 //$url = $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
411 $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
412 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
417 * Helper function that returns the HTML fragment to
418 * be displayed on listing mode, it includes actions
419 * like deletion/export/import...
421 function profiling_list_controls($listurl) {
424 $output = '<p class="centerpara buttons">';
425 $output .= ' <a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
432 * Helper function that looks for matchings of one string
433 * against an array of * wildchar patterns
435 function profiling_string_matches($string, $patterns) {
436 $patterns = explode(',', $patterns);
437 foreach ($patterns as $pattern) {
438 // Trim and prepare pattern
439 $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
440 // Don't process empty patterns
441 if (empty($pattern)) {
444 if (preg_match('~' . $pattern . '~', $string)) {
452 * Helper function that, given to floats, returns their numerical
453 * and percentual differences, propertly formated and cssstyled
455 function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
456 $numdiff = $number2 - $number1;
458 if ($number1 != $number2) {
459 $perdiff = $number1 != 0 ?
($number2 * 100 / $number1) - 100 : 0;
461 $sign = $number2 > $number1 ?
'+' : '';
462 $delta = abs($perdiff) > 0.25 ?
'Δ' : '≈';
463 $spanclass = $number2 > $number1 ?
'worse' : ($number1 > $number2 ?
'better' : 'same');
464 $importantclass= abs($perdiff) > 1 ?
' profiling_important' : '';
465 $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
466 $endspan = '</span>';
467 $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
468 $fperdiff = $sign . format_float($perdiff, $numdec);
469 return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
473 * Export profiling runs to a .mpr (moodle profile runs) file.
475 * This function gets an array of profiling runs (array of runids) and
476 * saves a .mpr file into destination for ulterior handling.
478 * Format of .mpr files:
479 * mpr files are simple zip packages containing these files:
480 * - moodle_profiling_runs.xml: Metadata about the information
481 * exported. Contains some header information (version and
482 * release of moodle, database, git hash - if available, date
483 * of export...) and a list of all the runids included in the
485 * - runid.xml: One file per each run detailed in the main file,
486 * containing the raw dump of the given runid in the profiling table.
488 * Possible improvement: Start storing some extra information in the
489 * profiling table for each run (moodle version, database, git hash...).
491 * @param array $runids list of runids to be exported.
492 * @param string $file filesystem fullpath to destination .mpr file.
493 * @return boolean the mpr file has been successfully exported (true) or no (false).
495 function profiling_export_runs(array $runids, $file) {
498 // Verify we have passed proper runids.
499 if (empty($runids)) {
503 // Verify all the passed runids do exist.
504 list ($insql, $inparams) = $DB->get_in_or_equal($runids);
505 $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
506 if ($reccount != count($runids)) {
510 // Verify the $file path is writeable.
511 $base = dirname($file);
512 if (!is_writable($base)) {
516 // Create temp directory where the temp information will be generated.
517 $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
520 // Generate the xml contents in the temp directory.
521 $status = profiling_export_generate($runids, $tmpdir);
523 // Package (zip) all the information into the final .mpr file.
525 $status = profiling_export_package($file, $tmpdir);
528 // Process finished ok, clean and return.
534 * Import a .mpr (moodle profile runs) file into moodle.
536 * See {@link profiling_export_runs()} for more details about the
537 * implementation of .mpr files.
539 * @param string $file filesystem fullpath to target .mpr file.
540 * @param string $commentprefix prefix to add to the comments of all the imported runs.
541 * @return boolean the mpr file has been successfully imported (true) or no (false).
543 function profiling_import_runs($file, $commentprefix = '') {
546 // Any problem with the file or its directory, abort.
547 if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
551 // Unzip the file into temp directory.
552 $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
553 $fp = get_file_packer('application/vnd.moodle.profiling');
554 $status = $fp->extract_to_pathname($file, $tmpdir);
556 // Look for master file and verify its format.
558 $mfile = $tmpdir . '/moodle_profiling_runs.xml';
559 if (!file_exists($mfile) or !is_readable($mfile)) {
562 $mdom = new DOMDocument();
563 if (!$mdom->load($mfile)) {
566 $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
571 // Verify all detail files exist and verify their format.
573 $runs = $mdom->getElementsByTagName('run');
574 foreach ($runs as $run) {
575 $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE
);
576 if (!file_exists($rfile) or !is_readable($rfile)) {
579 $rdom = new DOMDocument();
580 if (!$rdom->load($rfile)) {
583 $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
589 // Everything looks ok, let's import all the runs.
592 foreach ($runs as $run) {
593 $rfile = $tmpdir . '/' . $run->getAttribute('ref');
594 $rdom = new DOMDocument();
597 $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue
, PARAM_ALPHANUMEXT
);
598 $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue
, PARAM_CLEAN
);
599 $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue
, PARAM_INT
);
600 $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue
, PARAM_CLEAN
);
601 $runarr['timecreated'] = time(); // Now.
602 $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue
, PARAM_INT
);
603 $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue
, PARAM_INT
);
604 $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue
, PARAM_INT
);
605 $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue
, PARAM_INT
);
606 $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue
, PARAM_CLEAN
);
607 // If the runid does not exist, insert it.
608 if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
609 if (@gzuncompress
(base64_decode($runarr['data'])) === false) {
610 $runarr['data'] = base64_encode(gzcompress(base64_decode($runarr['data'])));
612 $DB->insert_record('profiling', $runarr);
619 // Clean the temp directory used for import.
626 * Generate the mpr contents (xml files) in the temporal directory.
628 * @param array $runids list of runids to be generated.
629 * @param string $tmpdir filesystem fullpath of tmp generation.
630 * @return boolean the mpr contents have been generated (true) or no (false).
632 function profiling_export_generate(array $runids, $tmpdir) {
635 if (empty($CFG->release
) ||
empty($CFG->version
)) {
636 // Some scripts may not have included version.php.
637 include($CFG->dirroot
.'/version.php');
638 $CFG->release
= $release;
639 $CFG->version
= $version;
642 // Calculate the header information to be sent to moodle_profiling_runs.xml.
643 $release = $CFG->release
;
644 $version = $CFG->version
;
645 $dbtype = $CFG->dbtype
;
646 $githash = phpunit_util
::get_git_hash();
649 // Create the xml output and writer for the main file.
650 $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
651 $mainxw = new xml_writer($mainxo);
655 $mainxw->begin_tag('moodle_profiling_runs');
657 // Send header information.
658 $mainxw->begin_tag('info');
659 $mainxw->full_tag('release', $release);
660 $mainxw->full_tag('version', $version);
661 $mainxw->full_tag('dbtype', $dbtype);
663 $mainxw->full_tag('githash', $githash);
665 $mainxw->full_tag('date', $date);
666 $mainxw->end_tag('info');
668 // Send information about runs.
669 $mainxw->begin_tag('runs');
670 foreach ($runids as $runid) {
671 // Get the run information from DB.
672 $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST
);
675 'ref' => $run->runid
. '.xml');
676 $mainxw->full_tag('run', null, $attributes);
677 // Create the individual run file.
678 $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
679 $runxw = new xml_writer($runxo);
681 $runxw->begin_tag('moodle_profiling_run');
682 $runxw->full_tag('id', $run->id
);
683 $runxw->full_tag('runid', $run->runid
);
684 $runxw->full_tag('url', $run->url
);
685 $runxw->full_tag('runreference', $run->runreference
);
686 $runxw->full_tag('runcomment', $run->runcomment
);
687 $runxw->full_tag('timecreated', $run->timecreated
);
688 $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime
);
689 $runxw->full_tag('totalcputime', $run->totalcputime
);
690 $runxw->full_tag('totalcalls', $run->totalcalls
);
691 $runxw->full_tag('totalmemory', $run->totalmemory
);
692 $runxw->full_tag('data', $run->data
);
693 $runxw->end_tag('moodle_profiling_run');
696 $mainxw->end_tag('runs');
697 $mainxw->end_tag('moodle_profiling_runs');
704 * Package (zip) the mpr contents (xml files) in the final location.
706 * @param string $file filesystem fullpath to destination .mpr file.
707 * @param string $tmpdir filesystem fullpath of tmp generation.
708 * @return boolean the mpr contents have been generated (true) or no (false).
710 function profiling_export_package($file, $tmpdir) {
711 // Get the list of files in $tmpdir.
712 $filestemp = get_directory_list($tmpdir, '', false, true, true);
715 // Add zip paths and fs paths to all them.
716 foreach ($filestemp as $filetemp) {
717 $files[$filetemp] = $tmpdir . '/' . $filetemp;
720 // Get the zip_packer.
721 $zippacker = get_file_packer('application/zip');
723 // Generate the packaged file.
724 $zippacker->archive_to_pathname($files, $file);
730 * Return the xml schema for the main import file.
735 function profiling_get_import_main_schema() {
737 <?xml version="1.0" encoding="UTF-8"?>
738 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
739 <xs:element name="moodle_profiling_runs">
742 <xs:element ref="info"/>
743 <xs:element ref="runs"/>
747 <xs:element name="info">
750 <xs:element type="xs:string" name="release"/>
751 <xs:element type="xs:decimal" name="version"/>
752 <xs:element type="xs:string" name="dbtype"/>
753 <xs:element type="xs:string" minOccurs="0" name="githash"/>
754 <xs:element type="xs:int" name="date"/>
758 <xs:element name="runs">
761 <xs:element maxOccurs="unbounded" ref="run"/>
765 <xs:element name="run">
767 <xs:attribute type="xs:int" name="id"/>
768 <xs:attribute type="xs:string" name="ref"/>
777 * Return the xml schema for each individual run import file.
782 function profiling_get_import_run_schema() {
784 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
785 <xs:element name="moodle_profiling_run">
788 <xs:element type="xs:int" name="id"/>
789 <xs:element type="xs:string" name="runid"/>
790 <xs:element type="xs:string" name="url"/>
791 <xs:element type="xs:int" name="runreference"/>
792 <xs:element type="xs:string" name="runcomment"/>
793 <xs:element type="xs:int" name="timecreated"/>
794 <xs:element type="xs:int" name="totalexecutiontime"/>
795 <xs:element type="xs:int" name="totalcputime"/>
796 <xs:element type="xs:int" name="totalcalls"/>
797 <xs:element type="xs:int" name="totalmemory"/>
798 <xs:element type="xs:string" name="data"/>
807 * Custom implementation of iXHProfRuns
809 * This class is one implementation of the iXHProfRuns interface, in charge
810 * of storing and retrieve profiling run data to/from DB (profiling table)
812 * The interface only defines two methods to be defined: get_run() and
813 * save_run() we'll be implementing some more in order to keep all the
814 * rest of information in our runs properly handled.
816 class moodle_xhprofrun
implements iXHProfRuns
{
818 protected $runid = null;
819 protected $url = null;
820 protected $totalexecutiontime = 0;
821 protected $totalcputime = 0;
822 protected $totalcalls = 0;
823 protected $totalmemory = 0;
824 protected $timecreated = 0;
826 public function __construct() {
827 $this->timecreated
= time();
831 * Given one runid and one type, return the run data
832 * and some extra info in run_desc from DB
834 * Note that $type is completely ignored
836 public function get_run($run_id, $type, &$run_desc) {
839 $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST
);
841 $this->runid
= $rec->runid
;
842 $this->url
= $rec->url
;
843 $this->totalexecutiontime
= $rec->totalexecutiontime
;
844 $this->totalcputime
= $rec->totalcputime
;
845 $this->totalcalls
= $rec->totalcalls
;
846 $this->totalmemory
= $rec->totalmemory
;
847 $this->timecreated
= $rec->timecreated
;
849 $run_desc = $this->url
. ($rec->runreference ?
' (R) ' : ' ') . ' - ' . s($rec->runcomment
);
851 // Handle historical runs that aren't compressed.
852 if (@gzuncompress
(base64_decode($rec->data
)) === false) {
853 return unserialize(base64_decode($rec->data
));
855 return unserialize(gzuncompress(base64_decode($rec->data
)));
860 * Given some run data, one type and, optionally, one runid
861 * store the information in DB
863 * Note that $type is completely ignored
865 public function save_run($xhprof_data, $type, $run_id = null) {
868 if (is_null($this->url
)) {
869 xhprof_error("Warning: You must use the prepare_run() method before saving it");
872 // Calculate runid if needed
873 $this->runid
= is_null($run_id) ?
md5($this->url
. '-' . uniqid()) : $run_id;
876 $this->totalexecutiontime
= $xhprof_data['main()']['wt'];
877 $this->totalcputime
= $xhprof_data['main()']['cpu'];
878 $this->totalcalls
= array_reduce($xhprof_data, array($this, 'sum_calls'));
879 $this->totalmemory
= $xhprof_data['main()']['mu'];
882 $rec = new stdClass();
883 $rec->runid
= $this->runid
;
884 $rec->url
= $this->url
;
885 $rec->totalexecutiontime
= $this->totalexecutiontime
;
886 $rec->totalcputime
= $this->totalcputime
;
887 $rec->totalcalls
= $this->totalcalls
;
888 $rec->totalmemory
= $this->totalmemory
;
889 $rec->timecreated
= $this->timecreated
;
891 // Send to database with compressed and endoded data.
892 if (empty($CFG->disableprofilingtodatabase
)) {
893 $rec->data
= base64_encode(gzcompress(serialize($xhprof_data), 9));
894 $DB->insert_record('profiling', $rec);
897 // Send raw data to plugins.
898 $rec->data
= $xhprof_data;
900 // Allow a plugin to take the trace data and process it.
901 if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
902 foreach ($pluginsfunction as $plugintype => $plugins) {
903 foreach ($plugins as $pluginfunction) {
904 $pluginfunction($rec);
910 // Calculate export variables.
911 $tempdir = 'profiling';
912 make_temp_directory($tempdir);
913 $runids = array($this->runid
);
914 $filename = $this->runid
. '.mpr';
915 $filepath = $CFG->tempdir
. '/' . $tempdir . '/' . $filename;
917 // Generate the mpr file and send it.
918 if (profiling_export_runs($runids, $filepath)) {
919 fprintf(STDERR
, "Profiling data saved to: ".$filepath."\n");
926 public function prepare_run($url) {
930 // Private API starts here
932 protected function sum_calls($sum, $data) {
933 return $sum +
$data['ct'];
938 * Simple subclass of {@link table_sql} that provides
939 * some custom formatters for various columns, in order
940 * to make the main profiles list nicer
942 class xhprof_table_sql
extends table_sql
{
944 protected $listurlmode = false;
947 * Get row classes to be applied based on row contents
949 function get_row_class($row) {
950 return $row->runreference ?
'referencerun' : ''; // apply class to reference runs
954 * Define it the table is in listurlmode or not, output will
955 * be different based on that
957 function set_listurlmode($listurlmode) {
958 $this->listurlmode
= $listurlmode;
962 * Format URL, so it points to last run for that url
964 protected function col_url($row) {
967 // Build the link to latest run for the script
968 $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url
, 'listurl' => $row->url
));
969 $scriptaction = $OUTPUT->action_link($scripturl, $row->url
);
971 // Decide, based on $this->listurlmode which actions to show
972 if ($this->listurlmode
) {
975 // Build link icon to script details (pix + url + actionlink)
976 $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url
));
977 $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url
));
978 $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
981 return $scriptaction . ' ' . $detailsaction;
985 * Format profiling date, human and pointing to run
987 protected function col_timecreated($row) {
989 $fdate = userdate($row->timecreated
, '%d %b %Y, %H:%M');
990 $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid
, 'listurl' => $row->url
));
991 return $OUTPUT->action_link($url, $fdate);
995 * Format execution time
997 protected function col_totalexecutiontime($row) {
998 return format_float($row->totalexecutiontime
/ 1000, 3) . ' ms';
1004 protected function col_totalcputime($row) {
1005 return format_float($row->totalcputime
/ 1000, 3) . ' ms';
1011 protected function col_totalmemory($row) {
1012 return format_float($row->totalmemory
/ 1024, 3) . ' KB';