AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @package core* @subpackage profiling* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();// Need some stuff from xhprof.require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_lib.php');require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php');// Need some stuff from moodle.require_once($CFG->libdir . '/tablelib.php');require_once($CFG->libdir . '/setuplib.php');require_once($CFG->libdir . '/filelib.php');require_once($CFG->libdir . '/phpunit/classes/util.php');require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');// TODO: Change the implementation below to proper profiling class./*** Returns if profiling is running, optionally setting it*/function profiling_is_running($value = null) {static $running = null;if (!is_null($value)) {$running = (bool)$value;}return $running;}/*** Returns if profiling has been saved, optionally setting it*/function profiling_is_saved($value = null) {static $saved = null;if (!is_null($value)) {$saved = (bool)$value;}return $saved;}/*** Whether PHP profiling is available.** This check ensures that one of the available PHP Profiling extensions is available.** @return bool*/function profiling_available() {$hasextension = extension_loaded('tideways_xhprof');$hasextension = $hasextension || extension_loaded('tideways');$hasextension = $hasextension || extension_loaded('xhprof');return $hasextension;}/*** Start profiling observing all the configuration*/function profiling_start() {global $CFG, $SESSION, $SCRIPT;// If profiling isn't available, nothing to startif (!profiling_available()) {return false;}// If profiling isn't enabled, nothing to startif (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {return false;}// If profiling is already running or saved, nothing to startif (profiling_is_running() || profiling_is_saved()) {return false;}// Set script (from global if available, else our own)$script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();// Get PGC variables$profileme = profiling_get_flag('PROFILEME') && !empty($CFG->profilingallowme);$dontprofileme = profiling_get_flag('DONTPROFILEME') && !empty($CFG->profilingallowme);$profileall = profiling_get_flag('PROFILEALL') && !empty($CFG->profilingallowall);$profileallstop = profiling_get_flag('PROFILEALLSTOP') && !empty($CFG->profilingallowall);// DONTPROFILEME detected, nothing to startif ($dontprofileme) {return false;}// PROFILEALLSTOP detected, clean the mark in seesion and continueif ($profileallstop && !empty($SESSION)) {unset($SESSION->profileall);}// PROFILEALL detected, set the mark in session and continueif ($profileall && !empty($SESSION)) {$SESSION->profileall = true;// SESSION->profileall detected, set $profileall} else if (!empty($SESSION->profileall)) {$profileall = true;}// Evaluate automatic (random) profiling if necessary$profileauto = false;if (!empty($CFG->profilingautofrec)) {$profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);}// Profile potentially slow pages.$profileslow = false;if (!empty($CFG->profilingslow) && !CLI_SCRIPT) {$profileslow = true;}// See if the $script matches any of the included patterns.$included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;$profileincluded = profiling_string_matches($script, $included);// See if the $script matches any of the excluded patterns$excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;$profileexcluded = profiling_string_matches($script, $excluded);// Decide if profile auto must happen (observe matchings)$profileauto = $profileauto && $profileincluded && !$profileexcluded;// Decide if profile by match must happen (only if profileauto is disabled)$profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);// Decide if slow profile has been excluded.$profileslow = $profileslow && !$profileexcluded;// If not auto, me, all, match have been detected, nothing to do.if (!$profileauto && !$profileme && !$profileall && !$profilematch && !$profileslow) {return false;}// If we have only been triggered by a *potentially* slow page then remember this for later.if ((!$profileauto && !$profileme && !$profileall && !$profilematch) && $profileslow) {$CFG->profilepotentialslowpage = microtime(true); // Neither $PAGE or $SESSION are guaranteed here.}// Arrived here, the script is going to be profiled, let's do it$ignore = array('call_user_func', 'call_user_func_array');if (extension_loaded('tideways_xhprof')) {tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY);} else if (extension_loaded('tideways')) {tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' => $ignore));} else {xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));}profiling_is_running(true);// Started, return truereturn true;}/*** Check for profiling flags in all possible places* @param string $flag name* @return boolean*/function profiling_get_flag($flag) {return !empty(getenv($flag)) ||isset($_COOKIE[$flag]) ||isset($_POST[$flag]) ||isset($_GET[$flag]);}/*** Stop profiling, gathering results and storing them*/function profiling_stop() {global $CFG, $DB, $SCRIPT;// If profiling isn't available, nothing to stopif (!profiling_available()) {return false;}// If profiling isn't enabled, nothing to stopif (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {return false;}// If profiling is not running or is already saved, nothing to stopif (!profiling_is_running() || profiling_is_saved()) {return false;}// Set script (from global if available, else our own)$script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();// Arrived here, profiling is running, stop and save everythingprofiling_is_running(false);if (extension_loaded('tideways_xhprof')) {$data = tideways_xhprof_disable();} else if (extension_loaded('tideways')) {$data = tideways_disable();} else {$data = xhprof_disable();}// We only save the run after ensuring the DB table exists// (this prevents problems with profiling runs enabled in// config.php before Moodle is installed. Rare but...$tables = $DB->get_tables();if (!in_array('profiling', $tables)) {return false;}// If we only profiled because it was potentially slow then...if (!empty($CFG->profilepotentialslowpage)) {$duration = microtime(true) - $CFG->profilepotentialslowpage;if ($duration < $CFG->profilingslow) {// Wasn't slow enough.return false;}$sql = "SELECT max(totalexecutiontime)FROM {profiling}WHERE url = ?";$slowest = $DB->get_field_sql($sql, array($script));if (!empty($slowest) && $duration * 1000000 < $slowest) {// Already have a worse profile stored.return false;}}$run = new moodle_xhprofrun();$run->prepare_run($script);$runid = $run->save_run($data, null);profiling_is_saved(true);// Prune old runsprofiling_prune_old_runs($runid);// Finished, return truereturn true;}function profiling_prune_old_runs($exception = 0) {global $CFG, $DB;// Setting to 0 = no pruneif (empty($CFG->profilinglifetime)) {return;}$cuttime = time() - ($CFG->profilinglifetime * 60);$params = array('cuttime' => $cuttime, 'exception' => $exception);$DB->delete_records_select('profiling', 'runreference = 0 ANDtimecreated < :cuttime ANDrunid != :exception', $params);}/*** Returns the path to the php script being requested** Note this function is a partial copy of initialise_fullme() and* setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and* friends. To be used by early profiling runs in situations where* $SCRIPT isn't defined yet** @return string absolute path (wwwroot based) of the script being executed*/function profiling_get_script() {global $CFG;$wwwroot = parse_url($CFG->wwwroot);if (!isset($wwwroot['path'])) {$wwwroot['path'] = '';}$wwwroot['path'] .= '/';$path = $_SERVER['SCRIPT_NAME'];if (strpos($path, $wwwroot['path']) === 0) {return substr($path, strlen($wwwroot['path']) - 1);}return '';}function profiling_urls($report, $runid, $runid2 = null) {global $CFG;$url = '';switch ($report) {case 'run':$url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;break;case 'diff':$url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&run2=' . $runid2;break;case 'graph':$url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;break;}return $url;}/*** Generate the output to print a profiling run including further actions you can then take.** @param object $run The profiling run object we are going to display.* @param array $prevreferences A list of run objects to list as comparison targets.* @return string The output to display on the screen for this run.*/function profiling_print_run($run, $prevreferences = null) {global $CFG, $OUTPUT;$output = '';// Prepare the runreference/runcomment form$checked = $run->runreference ? ' checked=checked' : '';$referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" ."<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>"."<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>"."<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>"."<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/> "."<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/> "."<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>"."</form>";$table = new html_table();$table->align = array('right', 'left');$table->tablealign = 'center';$table->attributes['class'] = 'profilingruntable';$table->colclasses = array('label', 'value');$table->data = array(array(get_string('runid', 'tool_profiling'), $run->runid),array(get_string('url'), $run->url),array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),array(get_string('calls', 'tool_profiling'), $run->totalcalls),array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),array(get_string('markreferencerun', 'tool_profiling'), $referenceform));$output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');// Add link to details$strviewdetails = get_string('viewdetails', 'tool_profiling');$url = profiling_urls('run', $run->runid);$output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');// If there are previous run(s) marked as reference, add link to diff.if ($prevreferences) {$table = new html_table();$table->align = array('left', 'left');$table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));$table->tablealign = 'center';$table->attributes['class'] = 'flexible generaltable generalbox';$table->colclasses = array('value', 'value', 'value');$table->data = array();$output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');foreach ($prevreferences as $reference) {$url = 'index.php?runid=' . $run->runid . '&runid2=' . $reference->runid . '&listurl=' . urlencode($run->url);$row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);$table->data[] = $row;}$output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');}// Add link to export this run.$strexport = get_string('exportthis', 'tool_profiling');$url = 'export.php?runid=' . $run->runid . '&listurl=' . urlencode($run->url);$output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');return $output;}function profiling_print_rundiff($run1, $run2) {global $CFG, $OUTPUT;$output = '';// Prepare the reference/comment information$referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .($run1->runcomment ? ' - ' . s($run1->runcomment) : '');$referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .($run2->runcomment ? ' - ' . s($run2->runcomment) : '');// Calculate global differences$diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);$diffcputime = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);$diffcalls = profiling_get_difference($run1->totalcalls, $run2->totalcalls);$diffmemory = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);$table = new html_table();$table->align = array('right', 'left', 'left', 'left');$table->tablealign = 'center';$table->attributes['class'] = 'profilingruntable';$table->colclasses = array('label', 'value1', 'value2');$table->data = array(array(get_string('runid', 'tool_profiling'),'<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>','<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),array(get_string('url'), $run1->url, $run2->url),array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),userdate($run2->timecreated, '%d %B %Y, %H:%M')),array(get_string('executiontime', 'tool_profiling'),format_float($run1->totalexecutiontime / 1000, 3) . ' ms',format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),array(get_string('cputime', 'tool_profiling'),format_float($run1->totalcputime / 1000, 3) . ' ms',format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),array(get_string('memory', 'tool_profiling'),format_float($run1->totalmemory / 1024, 0) . ' KB',format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));$output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');// Add link to details$strviewdetails = get_string('viewdiffdetails', 'tool_profiling');$url = profiling_urls('diff', $run1->runid, $run2->runid);//$url = $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;$output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');return $output;}/*** Helper function that returns the HTML fragment to* be displayed on listing mode, it includes actions* like deletion/export/import...*/function profiling_list_controls($listurl) {global $CFG;$output = '<p class="centerpara buttons">';$output .= ' <a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';$output .= '</p>';return $output;}/*** Helper function that looks for matchings of one string* against an array of * wildchar patterns*/function profiling_string_matches($string, $patterns) {$patterns = preg_split("/\n|,/", $patterns);foreach ($patterns as $pattern) {// Trim and prepare pattern$pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));// Don't process empty patternsif (empty($pattern)) {continue;}if (preg_match('~^' . $pattern . '$~', $string)) {return true;}}return false;}/*** Helper function that, given to floats, returns their numerical* and percentual differences, propertly formated and cssstyled*/function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {$numdiff = $number2 - $number1;$perdiff = 0;if ($number1 != $number2) {$perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;}$sign = $number2 > $number1 ? '+' : '';$delta = abs($perdiff) > 0.25 ? 'Δ' : '≈';$spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');$importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';$startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';$endspan = '</span>';$fnumdiff = $sign . format_float($numdiff / $factor, $numdec);$fperdiff = $sign . format_float($perdiff, $numdec);return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;}/*** Export profiling runs to a .mpr (moodle profile runs) file.** This function gets an array of profiling runs (array of runids) and* saves a .mpr file into destination for ulterior handling.** Format of .mpr files:* mpr files are simple zip packages containing these files:* - moodle_profiling_runs.xml: Metadata about the information* exported. Contains some header information (version and* release of moodle, database, git hash - if available, date* of export...) and a list of all the runids included in the* export.* - runid.xml: One file per each run detailed in the main file,* containing the raw dump of the given runid in the profiling table.** Possible improvement: Start storing some extra information in the* profiling table for each run (moodle version, database, git hash...).** @param array $runids list of runids to be exported.* @param string $file filesystem fullpath to destination .mpr file.* @return boolean the mpr file has been successfully exported (true) or no (false).*/function profiling_export_runs(array $runids, $file) {global $CFG, $DB;// Verify we have passed proper runids.if (empty($runids)) {return false;}// Verify all the passed runids do exist.list ($insql, $inparams) = $DB->get_in_or_equal($runids);$reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);if ($reccount != count($runids)) {return false;}// Verify the $file path is writeable.$base = dirname($file);if (!is_writable($base)) {return false;}// Create temp directory where the temp information will be generated.$tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));mkdir($tmpdir);// Generate the xml contents in the temp directory.$status = profiling_export_generate($runids, $tmpdir);// Package (zip) all the information into the final .mpr file.if ($status) {$status = profiling_export_package($file, $tmpdir);}// Process finished ok, clean and return.fulldelete($tmpdir);return $status;}/*** Import a .mpr (moodle profile runs) file into moodle.** See {@link profiling_export_runs()} for more details about the* implementation of .mpr files.** @param string $file filesystem fullpath to target .mpr file.* @param string $commentprefix prefix to add to the comments of all the imported runs.* @return boolean the mpr file has been successfully imported (true) or no (false).*/function profiling_import_runs($file, $commentprefix = '') {global $DB;// Any problem with the file or its directory, abort.if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {return false;}// Unzip the file into temp directory.$tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);$fp = get_file_packer('application/vnd.moodle.profiling');$status = $fp->extract_to_pathname($file, $tmpdir);// Look for master file and verify its format.if ($status) {$mfile = $tmpdir . '/moodle_profiling_runs.xml';if (!file_exists($mfile) or !is_readable($mfile)) {$status = false;} else {$mdom = new DOMDocument();if (!$mdom->load($mfile)) {$status = false;} else {$status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());}}}// Verify all detail files exist and verify their format.if ($status) {$runs = $mdom->getElementsByTagName('run');foreach ($runs as $run) {$rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);if (!file_exists($rfile) or !is_readable($rfile)) {$status = false;} else {$rdom = new DOMDocument();if (!$rdom->load($rfile)) {$status = false;} else {$status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());}}}}// Everything looks ok, let's import all the runs.if ($status) {reset($runs);foreach ($runs as $run) {$rfile = $tmpdir . '/' . $run->getAttribute('ref');$rdom = new DOMDocument();$rdom->load($rfile);$runarr = array();$runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);$runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);$runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);$runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);$runarr['timecreated'] = time(); // Now.$runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);$runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);$runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);$runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);$runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);// If the runid does not exist, insert it.if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {if (@gzuncompress(base64_decode($runarr['data'])) === false) {$runarr['data'] = base64_encode(gzcompress(base64_decode($runarr['data'])));}$DB->insert_record('profiling', $runarr);} else {return false;}}}// Clean the temp directory used for import.remove_dir($tmpdir);return $status;}/*** Generate the mpr contents (xml files) in the temporal directory.** @param array $runids list of runids to be generated.* @param string $tmpdir filesystem fullpath of tmp generation.* @return boolean the mpr contents have been generated (true) or no (false).*/function profiling_export_generate(array $runids, $tmpdir) {global $CFG, $DB;if (empty($CFG->release) || empty($CFG->version)) {// Some scripts may not have included version.php.include($CFG->dirroot.'/version.php');$CFG->release = $release;$CFG->version = $version;}// Calculate the header information to be sent to moodle_profiling_runs.xml.$release = $CFG->release;$version = $CFG->version;$dbtype = $CFG->dbtype;$githash = phpunit_util::get_git_hash();$date = time();// Create the xml output and writer for the main file.$mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');$mainxw = new xml_writer($mainxo);// Output begins.$mainxw->start();$mainxw->begin_tag('moodle_profiling_runs');// Send header information.$mainxw->begin_tag('info');$mainxw->full_tag('release', $release);$mainxw->full_tag('version', $version);$mainxw->full_tag('dbtype', $dbtype);if ($githash) {$mainxw->full_tag('githash', $githash);}$mainxw->full_tag('date', $date);$mainxw->end_tag('info');// Send information about runs.$mainxw->begin_tag('runs');foreach ($runids as $runid) {// Get the run information from DB.$run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);$attributes = array('id' => $run->id,'ref' => $run->runid . '.xml');$mainxw->full_tag('run', null, $attributes);// Create the individual run file.$runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);$runxw = new xml_writer($runxo);$runxw->start();$runxw->begin_tag('moodle_profiling_run');$runxw->full_tag('id', $run->id);$runxw->full_tag('runid', $run->runid);$runxw->full_tag('url', $run->url);$runxw->full_tag('runreference', $run->runreference);$runxw->full_tag('runcomment', $run->runcomment);$runxw->full_tag('timecreated', $run->timecreated);$runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);$runxw->full_tag('totalcputime', $run->totalcputime);$runxw->full_tag('totalcalls', $run->totalcalls);$runxw->full_tag('totalmemory', $run->totalmemory);$runxw->full_tag('data', $run->data);$runxw->end_tag('moodle_profiling_run');$runxw->stop();}$mainxw->end_tag('runs');$mainxw->end_tag('moodle_profiling_runs');$mainxw->stop();return true;}/*** Package (zip) the mpr contents (xml files) in the final location.** @param string $file filesystem fullpath to destination .mpr file.* @param string $tmpdir filesystem fullpath of tmp generation.* @return boolean the mpr contents have been generated (true) or no (false).*/function profiling_export_package($file, $tmpdir) {// Get the list of files in $tmpdir.$filestemp = get_directory_list($tmpdir, '', false, true, true);$files = array();// Add zip paths and fs paths to all them.foreach ($filestemp as $filetemp) {$files[$filetemp] = $tmpdir . '/' . $filetemp;}// Get the zip_packer.$zippacker = get_file_packer('application/zip');// Generate the packaged file.$zippacker->archive_to_pathname($files, $file);return true;}/*** Return the xml schema for the main import file.** @return string**/function profiling_get_import_main_schema() {$schema = <<<EOS<?xml version="1.0" encoding="UTF-8"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"><xs:element name="moodle_profiling_runs"><xs:complexType><xs:sequence><xs:element ref="info"/><xs:element ref="runs"/></xs:sequence></xs:complexType></xs:element><xs:element name="info"><xs:complexType><xs:sequence><xs:element type="xs:string" name="release"/><xs:element type="xs:decimal" name="version"/><xs:element type="xs:string" name="dbtype"/><xs:element type="xs:string" minOccurs="0" name="githash"/><xs:element type="xs:int" name="date"/></xs:sequence></xs:complexType></xs:element><xs:element name="runs"><xs:complexType><xs:sequence><xs:element maxOccurs="unbounded" ref="run"/></xs:sequence></xs:complexType></xs:element><xs:element name="run"><xs:complexType><xs:attribute type="xs:int" name="id"/><xs:attribute type="xs:string" name="ref"/></xs:complexType></xs:element></xs:schema>EOS;return $schema;}/*** Return the xml schema for each individual run import file.** @return string**/function profiling_get_import_run_schema() {$schema = <<<EOS<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"><xs:element name="moodle_profiling_run"><xs:complexType><xs:sequence><xs:element type="xs:int" name="id"/><xs:element type="xs:string" name="runid"/><xs:element type="xs:string" name="url"/><xs:element type="xs:int" name="runreference"/><xs:element type="xs:string" name="runcomment"/><xs:element type="xs:int" name="timecreated"/><xs:element type="xs:integer" name="totalexecutiontime"/><xs:element type="xs:integer" name="totalcputime"/><xs:element type="xs:integer" name="totalcalls"/><xs:element type="xs:integer" name="totalmemory"/><xs:element type="xs:string" name="data"/></xs:sequence></xs:complexType></xs:element></xs:schema>EOS;return $schema;}/*** Custom implementation of iXHProfRuns** This class is one implementation of the iXHProfRuns interface, in charge* of storing and retrieve profiling run data to/from DB (profiling table)** The interface only defines two methods to be defined: get_run() and* save_run() we'll be implementing some more in order to keep all the* rest of information in our runs properly handled.*/class moodle_xhprofrun implements iXHProfRuns {protected $runid = null;protected $url = null;protected $totalexecutiontime = 0;protected $totalcputime = 0;protected $totalcalls = 0;protected $totalmemory = 0;protected $timecreated = 0;/** @var bool Decide if we want to reduce profiling data or no */protected bool $reducedata = false;public function __construct() {$this->timecreated = time();}/*** Given one runid and one type, return the run data* and some extra info in run_desc from DB** Note that $type is completely ignored*/public function get_run($run_id, $type, &$run_desc) {global $DB;$rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);$this->runid = $rec->runid;$this->url = $rec->url;$this->totalexecutiontime = $rec->totalexecutiontime;$this->totalcputime = $rec->totalcputime;$this->totalcalls = $rec->totalcalls;$this->totalmemory = $rec->totalmemory;$this->timecreated = $rec->timecreated;$run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);// Handle historical runs that aren't compressed.if (@gzuncompress(base64_decode($rec->data)) === false) {return unserialize(base64_decode($rec->data));} else {$info = unserialize(gzuncompress(base64_decode($rec->data)));if (!$this->reducedata) {// We want to return the full data.return $info;}// We want to apply some transformations here, in order to reduce// the information for some complex (too many levels) cases.return $this->reduce_run_data($info);}}/*** Given some run data, one type and, optionally, one runid* store the information in DB** Note that $type is completely ignored*/public function save_run($xhprof_data, $type, $run_id = null) {global $DB, $CFG;if (is_null($this->url)) {xhprof_error("Warning: You must use the prepare_run() method before saving it");}// Calculate runid if needed$this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;// Calculate totals$this->totalexecutiontime = $xhprof_data['main()']['wt'];$this->totalcputime = $xhprof_data['main()']['cpu'];$this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));$this->totalmemory = $xhprof_data['main()']['mu'];// Prepare data$rec = new stdClass();$rec->runid = $this->runid;$rec->url = $this->url;$rec->totalexecutiontime = $this->totalexecutiontime;$rec->totalcputime = $this->totalcputime;$rec->totalcalls = $this->totalcalls;$rec->totalmemory = $this->totalmemory;$rec->timecreated = $this->timecreated;// Send to database with compressed and endoded data.if (empty($CFG->disableprofilingtodatabase)) {$rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));$DB->insert_record('profiling', $rec);}// Send raw data to plugins.$rec->data = $xhprof_data;// Allow a plugin to take the trace data and process it.if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {foreach ($pluginsfunction as $plugintype => $plugins) {foreach ($plugins as $pluginfunction) {$pluginfunction($rec);}}}if (PHPUNIT_TEST) {// Calculate export variables.$tempdir = 'profiling';make_temp_directory($tempdir);$runids = array($this->runid);$filename = $this->runid . '.mpr';$filepath = $CFG->tempdir . '/' . $tempdir . '/' . $filename;// Generate the mpr file and send it.if (profiling_export_runs($runids, $filepath)) {fprintf(STDERR, "Profiling data saved to: ".$filepath."\n");}}return $this->runid;}public function prepare_run($url) {$this->url = $url;}/*** Enable or disable reducing profiling data.** @param bool $reducedata Decide if we want to reduce profiling data (true) or no (false).*/public function set_reducedata(bool $reducedata): void {$this->reducedata = $reducedata;}// Private API starts here.protected function sum_calls($sum, $data) {return $sum + $data['ct'];}/*** Reduce the run data to a more manageable size.** This removes from the run data all the entries that* are matching a group of regular expressions.** The main use is to remove all the calls between "__Mustache"* functions, which don't provide any useful information and* make the call-graph too complex to be handled.** @param array $info The xhprof run data, original array.* @return array The xhprof run data, reduced array.*/protected function reduce_run_data(array $info): array {// Define which (regular expressions) we want to remove. Already escaped if needed to, please.$toremove = ['__Mustache.*==>__Mustache.*', // All __Mustache to __Mustache calls.];// Build the regular expression to be used.$regexp = '/^(' . implode('|', $toremove) . ')$/';// Given that the keys of the array have the format "parent==>child"// we want to rebuild the array with the same structure but// topologically sorted (parents always before children).// Note that we do this exclusively to guarantee that the// second pass (see below) works properly in all cases because,// without it, we may need to perform N (while loop) second passes.$sorted = $this->xhprof_topo_sort($info);// To keep track of removed and remaining (child-parent) pairs.$removed = [];$remaining = [];// First pass, we are going to remove all the elements which// both parent and child are __Mustache function calls.foreach ($sorted as $key => $value) {if (!str_contains($key, '==>')) {$parent = 'NULL';$child = $key;} else {[$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.}if (preg_match($regexp, $key)) {unset($sorted[$key]);$removed[$child][$parent] = true;} else {$remaining[$child][$parent] = true;}}// Second pass, we are going to remove all the elements which// parent was removed by first pass and doesn't appear anymore// as a child of anything (aka, they have become orphaned).// Note, that thanks to the topological sorting, we can be sure// one unique pass is enough. Without it, we may need to perform// N (while loop) second passes.foreach ($sorted as $key => $value) {if (!str_contains($key, '==>')) {$parent = 'NULL';$child = $key;} else {[$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.}if (isset($removed[$parent]) && !isset($remaining[$parent])) {unset($sorted[$key]);$removed[$child][$parent] = true;unset($remaining[$child][$parent]);// If this was the last parent of this child, remove it completely from the remaining array.if (empty($remaining[$child])) {unset($remaining[$child]);}}}// We are done, let's return the reduced array.return $sorted;}/*** Sort the xhprof run pseudo-topologically, so all parents are always before their children.** Note that this is not a proper, complex, recursive topological sorting algorithm, returning* nodes that later have to be converted back to xhprof "pairs" but, instead, does the specific* work to get those parent==>child (2 levels only) "pairs" sorted (parents always before children).** @param array $info The xhprof run data, original array.** @return array The xhprof run data, sorted array.*/protected function xhprof_topo_sort(array $info): array {$sorted = [];$visited = [];$remaining = $info;do {$newremaining = [];foreach ($remaining as $key => $value) {// If we already have visited this element, we can skip it.if (isset($visited[$key])) {continue;}if (!str_contains($key, '==>')) {// It's a root element, we can add it to the sorted array.$sorted[$key] = $info[$key];$visited[$key] = true;} else {[$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.if (isset($visited[$parent])) {// Parent already visited, we can add any children to the sorted array.$sorted[$key] = $info[$key];$visited[$child] = true;} else {// Cannot add this yet, we need to wait for the parent.$newremaining[$key] = $value;}}}// Protection against infinite loops.if (count($remaining) === count($newremaining)) {$remaining = []; // So we exit the do...while loop.} else {$remaining = $newremaining; // There is still work to do.}} while (count($remaining) > 0);// We are done, let's return the sorted array.return $sorted;}}/*** Simple subclass of {@link table_sql} that provides* some custom formatters for various columns, in order* to make the main profiles list nicer*/class xhprof_table_sql extends table_sql {protected $listurlmode = false;/*** Get row classes to be applied based on row contents*/function get_row_class($row) {return $row->runreference ? 'referencerun' : ''; // apply class to reference runs}/*** Define it the table is in listurlmode or not, output will* be different based on that*/function set_listurlmode($listurlmode) {$this->listurlmode = $listurlmode;}/*** Format URL, so it points to last run for that url*/protected function col_url($row) {global $OUTPUT;// Build the link to latest run for the script$scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));$scriptaction = $OUTPUT->action_link($scripturl, $row->url);// Decide, based on $this->listurlmode which actions to showif ($this->listurlmode) {$detailsaction = '';} else {// Build link icon to script details (pix + url + actionlink)$detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));$detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));$detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);}return $scriptaction . ' ' . $detailsaction;}/*** Format profiling date, human and pointing to run*/protected function col_timecreated($row) {global $OUTPUT;$fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');$url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));return $OUTPUT->action_link($url, $fdate);}/*** Format execution time*/protected function col_totalexecutiontime($row) {return format_float($row->totalexecutiontime / 1000, 3) . ' ms';}/*** Format cpu time*/protected function col_totalcputime($row) {return format_float($row->totalcputime / 1000, 3) . ' ms';}/*** Format memory*/protected function col_totalmemory($row) {return format_float($row->totalmemory / 1024, 3) . ' KB';}}