Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  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/>.
  16  
  17  /**
  18   * @package    core
  19   * @subpackage profiling
  20   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  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');
  37  
  38  // TODO: Change the implementation below to proper profiling class.
  39  
  40  /**
  41   * Returns if profiling is running, optionally setting it
  42   */
  43  function profiling_is_running($value = null) {
  44      static $running = null;
  45  
  46      if (!is_null($value)) {
  47          $running = (bool)$value;
  48      }
  49  
  50      return $running;
  51  }
  52  
  53  /**
  54   * Returns if profiling has been saved, optionally setting it
  55   */
  56  function profiling_is_saved($value = null) {
  57      static $saved = null;
  58  
  59      if (!is_null($value)) {
  60          $saved = (bool)$value;
  61      }
  62  
  63      return $saved;
  64  }
  65  
  66  /**
  67   * Whether PHP profiling is available.
  68   *
  69   * This check ensures that one of the available PHP Profiling extensions is available.
  70   *
  71   * @return  bool
  72   */
  73  function profiling_available() {
  74      $hasextension = extension_loaded('tideways_xhprof');
  75      $hasextension = $hasextension || extension_loaded('tideways');
  76      $hasextension = $hasextension || extension_loaded('xhprof');
  77  
  78      return $hasextension;
  79  }
  80  
  81  /**
  82   * Start profiling observing all the configuration
  83   */
  84  function profiling_start() {
  85      global $CFG, $SESSION, $SCRIPT;
  86  
  87      // If profiling isn't available, nothing to start
  88      if (!profiling_available()) {
  89          return false;
  90      }
  91  
  92      // If profiling isn't enabled, nothing to start
  93      if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
  94          return false;
  95      }
  96  
  97      // If profiling is already running or saved, nothing to start
  98      if (profiling_is_running() || profiling_is_saved()) {
  99          return false;
 100      }
 101  
 102      // Set script (from global if available, else our own)
 103      $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
 104  
 105      // Get PGC variables
 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);
 118  
 119      // DONTPROFILEME detected, nothing to start
 120      if ($dontprofileme) {
 121          return false;
 122      }
 123  
 124      // PROFILEALLSTOP detected, clean the mark in seesion and continue
 125      if ($profileallstop && !empty($SESSION)) {
 126          unset($SESSION->profileall);
 127      }
 128  
 129      // PROFILEALL detected, set the mark in session and continue
 130      if ($profileall && !empty($SESSION)) {
 131          $SESSION->profileall = true;
 132  
 133      // SESSION->profileall detected, set $profileall
 134      } else if (!empty($SESSION->profileall)) {
 135          $profileall = true;
 136      }
 137  
 138      // Evaluate automatic (random) profiling if necessary
 139      $profileauto = false;
 140      if (!empty($CFG->profilingautofrec)) {
 141          $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
 142      }
 143  
 144      // Profile potentially slow pages.
 145      $profileslow = false;
 146      if (!empty($CFG->profilingslow) && !CLI_SCRIPT) {
 147          $profileslow = true;
 148      }
 149  
 150      // See if the $script matches any of the included patterns.
 151      $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
 152      $profileincluded = profiling_string_matches($script, $included);
 153  
 154      // See if the $script matches any of the excluded patterns
 155      $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
 156      $profileexcluded = profiling_string_matches($script, $excluded);
 157  
 158      // Decide if profile auto must happen (observe matchings)
 159      $profileauto = $profileauto && $profileincluded && !$profileexcluded;
 160  
 161      // Decide if profile by match must happen (only if profileauto is disabled)
 162      $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);
 163  
 164      // Decide if slow profile has been excluded.
 165      $profileslow = $profileslow && !$profileexcluded;
 166  
 167      // If not auto, me, all, match have been detected, nothing to do.
 168      if (!$profileauto && !$profileme && !$profileall && !$profilematch && !$profileslow) {
 169          return false;
 170      }
 171  
 172      // If we have only been triggered by a *potentially* slow page then remember this for later.
 173      if ((!$profileauto && !$profileme && !$profileall && !$profilematch) && $profileslow) {
 174          $CFG->profilepotentialslowpage = microtime(true); // Neither $PAGE or $SESSION are guaranteed here.
 175      }
 176  
 177      // Arrived here, the script is going to be profiled, let's do it
 178      $ignore = array('call_user_func', 'call_user_func_array');
 179      if (extension_loaded('tideways_xhprof')) {
 180          tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY);
 181      } else if (extension_loaded('tideways')) {
 182          tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
 183      } else {
 184          xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
 185      }
 186      profiling_is_running(true);
 187  
 188      // Started, return true
 189      return true;
 190  }
 191  
 192  /**
 193   * Stop profiling, gathering results and storing them
 194   */
 195  function profiling_stop() {
 196      global $CFG, $DB, $SCRIPT;
 197  
 198      // If profiling isn't available, nothing to stop
 199      if (!profiling_available()) {
 200          return false;
 201      }
 202  
 203      // If profiling isn't enabled, nothing to stop
 204      if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
 205          return false;
 206      }
 207  
 208      // If profiling is not running or is already saved, nothing to stop
 209      if (!profiling_is_running() || profiling_is_saved()) {
 210          return false;
 211      }
 212  
 213      // Set script (from global if available, else our own)
 214      $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
 215  
 216      // Arrived here, profiling is running, stop and save everything
 217      profiling_is_running(false);
 218      if (extension_loaded('tideways_xhprof')) {
 219          $data = tideways_xhprof_disable();
 220      } else if (extension_loaded('tideways')) {
 221          $data = tideways_disable();
 222      } else {
 223          $data = xhprof_disable();
 224      }
 225  
 226      // We only save the run after ensuring the DB table exists
 227      // (this prevents problems with profiling runs enabled in
 228      // config.php before Moodle is installed. Rare but...
 229      $tables = $DB->get_tables();
 230      if (!in_array('profiling', $tables)) {
 231          return false;
 232      }
 233  
 234      // If we only profiled because it was potentially slow then...
 235      if (!empty($CFG->profilepotentialslowpage)) {
 236          $duration = microtime(true) - $CFG->profilepotentialslowpage;
 237          if ($duration < $CFG->profilingslow) {
 238              // Wasn't slow enough.
 239              return false;
 240          }
 241  
 242          $sql = "SELECT max(totalexecutiontime)
 243                    FROM {profiling}
 244                   WHERE url = ?";
 245          $slowest = $DB->get_field_sql($sql, array($script));
 246          if (!empty($slowest) && $duration * 1000000 < $slowest) {
 247              // Already have a worse profile stored.
 248              return false;
 249          }
 250      }
 251  
 252      $run = new moodle_xhprofrun();
 253      $run->prepare_run($script);
 254      $runid = $run->save_run($data, null);
 255      profiling_is_saved(true);
 256  
 257      // Prune old runs
 258      profiling_prune_old_runs($runid);
 259  
 260      // Finished, return true
 261      return true;
 262  }
 263  
 264  function profiling_prune_old_runs($exception = 0) {
 265      global $CFG, $DB;
 266  
 267      // Setting to 0 = no prune
 268      if (empty($CFG->profilinglifetime)) {
 269          return;
 270      }
 271  
 272      $cuttime = time() - ($CFG->profilinglifetime * 60);
 273      $params = array('cuttime' => $cuttime, 'exception' => $exception);
 274  
 275      $DB->delete_records_select('profiling', 'runreference = 0 AND
 276                                               timecreated < :cuttime AND
 277                                               runid != :exception', $params);
 278  }
 279  
 280  /**
 281   * Returns the path to the php script being requested
 282   *
 283   * Note this function is a partial copy of initialise_fullme() and
 284   * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
 285   * friends. To be used by early profiling runs in situations where
 286   * $SCRIPT isn't defined yet
 287   *
 288   * @return string absolute path (wwwroot based) of the script being executed
 289   */
 290  function profiling_get_script() {
 291      global $CFG;
 292  
 293      $wwwroot = parse_url($CFG->wwwroot);
 294  
 295      if (!isset($wwwroot['path'])) {
 296          $wwwroot['path'] = '';
 297      }
 298      $wwwroot['path'] .= '/';
 299  
 300      $path = $_SERVER['SCRIPT_NAME'];
 301  
 302      if (strpos($path, $wwwroot['path']) === 0) {
 303          return substr($path, strlen($wwwroot['path']) - 1);
 304      }
 305      return '';
 306  }
 307  
 308  function profiling_urls($report, $runid, $runid2 = null) {
 309      global $CFG;
 310  
 311      $url = '';
 312      switch ($report) {
 313          case 'run':
 314              $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
 315              break;
 316          case 'diff':
 317              $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
 318              break;
 319          case 'graph':
 320              $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
 321              break;
 322      }
 323      return $url;
 324  }
 325  
 326  /**
 327   * Generate the output to print a profiling run including further actions you can then take.
 328   *
 329   * @param object $run The profiling run object we are going to display.
 330   * @param array $prevreferences A list of run objects to list as comparison targets.
 331   * @return string The output to display on the screen for this run.
 332   */
 333  function profiling_print_run($run, $prevreferences = null) {
 334      global $CFG, $OUTPUT;
 335  
 336      $output = '';
 337  
 338      // Prepare the runreference/runcomment form
 339      $checked = $run->runreference ? ' checked=checked' : '';
 340      $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
 341                       "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
 342                       "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
 343                       "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
 344                       "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
 345                       "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
 346                       "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
 347                       "</form>";
 348  
 349      $table = new html_table();
 350      $table->align = array('right', 'left');
 351      $table->tablealign = 'center';
 352      $table->attributes['class'] = 'profilingruntable';
 353      $table->colclasses = array('label', 'value');
 354      $table->data = array(
 355         array(get_string('runid', 'tool_profiling'), $run->runid),
 356         array(get_string('url'), $run->url),
 357         array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
 358         array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
 359         array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
 360         array(get_string('calls', 'tool_profiling'), $run->totalcalls),
 361         array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
 362         array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
 363      $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
 364      // Add link to details
 365      $strviewdetails = get_string('viewdetails', 'tool_profiling');
 366      $url = profiling_urls('run', $run->runid);
 367      $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
 368                                  'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
 369  
 370      // If there are previous run(s) marked as reference, add link to diff.
 371      if ($prevreferences) {
 372          $table = new html_table();
 373          $table->align = array('left', 'left');
 374          $table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
 375          $table->tablealign = 'center';
 376          $table->attributes['class'] = 'flexible generaltable generalbox';
 377          $table->colclasses = array('value', 'value', 'value');
 378          $table->data = array();
 379  
 380          $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');
 381  
 382          foreach ($prevreferences as $reference) {
 383              $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $reference->runid . '&amp;listurl=' . urlencode($run->url);
 384              $row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);
 385              $table->data[] = $row;
 386          }
 387          $output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');
 388  
 389      }
 390      // Add link to export this run.
 391      $strexport = get_string('exportthis', 'tool_profiling');
 392      $url = 'export.php?runid=' . $run->runid . '&amp;listurl=' . urlencode($run->url);
 393      $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');
 394  
 395      return $output;
 396  }
 397  
 398  function profiling_print_rundiff($run1, $run2) {
 399      global $CFG, $OUTPUT;
 400  
 401      $output = '';
 402  
 403      // Prepare the reference/comment information
 404      $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
 405                        ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
 406      $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
 407                        ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
 408  
 409      // Calculate global differences
 410      $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
 411      $diffcputime       = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
 412      $diffcalls         = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
 413      $diffmemory        = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
 414  
 415      $table = new html_table();
 416      $table->align = array('right', 'left', 'left', 'left');
 417      $table->tablealign = 'center';
 418      $table->attributes['class'] = 'profilingruntable';
 419      $table->colclasses = array('label', 'value1', 'value2');
 420      $table->data = array(
 421         array(get_string('runid', 'tool_profiling'),
 422             '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
 423             '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
 424         array(get_string('url'), $run1->url, $run2->url),
 425         array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
 426             userdate($run2->timecreated, '%d %B %Y, %H:%M')),
 427         array(get_string('executiontime', 'tool_profiling'),
 428             format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
 429             format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
 430         array(get_string('cputime', 'tool_profiling'),
 431             format_float($run1->totalcputime / 1000, 3) . ' ms',
 432             format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
 433         array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
 434         array(get_string('memory', 'tool_profiling'),
 435             format_float($run1->totalmemory / 1024, 0) . ' KB',
 436             format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
 437         array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
 438      $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
 439      // Add link to details
 440      $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
 441      $url = profiling_urls('diff', $run1->runid, $run2->runid);
 442      //$url =  $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
 443      $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
 444                                'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
 445      return $output;
 446  }
 447  
 448  /**
 449   * Helper function that returns the HTML fragment to
 450   * be displayed on listing mode, it includes actions
 451   * like deletion/export/import...
 452   */
 453  function profiling_list_controls($listurl) {
 454      global $CFG;
 455  
 456      $output = '<p class="centerpara buttons">';
 457      $output .= '&nbsp;<a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
 458      $output .= '</p>';
 459  
 460      return $output;
 461  }
 462  
 463  /**
 464   * Helper function that looks for matchings of one string
 465   * against an array of * wildchar patterns
 466   */
 467  function profiling_string_matches($string, $patterns) {
 468     $patterns = preg_split("/\n|,/", $patterns);
 469      foreach ($patterns as $pattern) {
 470          // Trim and prepare pattern
 471          $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
 472          // Don't process empty patterns
 473          if (empty($pattern)) {
 474              continue;
 475          }
 476          if (preg_match('~^' . $pattern . '$~', $string)) {
 477              return true;
 478          }
 479      }
 480      return false;
 481  }
 482  
 483  /**
 484   * Helper function that, given to floats, returns their numerical
 485   * and percentual differences, propertly formated and cssstyled
 486   */
 487  function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
 488      $numdiff = $number2 - $number1;
 489      $perdiff = 0;
 490      if ($number1 != $number2) {
 491          $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
 492      }
 493      $sign      = $number2 > $number1 ? '+' : '';
 494      $delta     = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
 495      $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
 496      $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
 497      $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
 498      $endspan   = '</span>';
 499      $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
 500      $fperdiff = $sign . format_float($perdiff, $numdec);
 501      return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
 502  }
 503  
 504  /**
 505   * Export profiling runs to a .mpr (moodle profile runs) file.
 506   *
 507   * This function gets an array of profiling runs (array of runids) and
 508   * saves a .mpr file into destination for ulterior handling.
 509   *
 510   * Format of .mpr files:
 511   *   mpr files are simple zip packages containing these files:
 512   *     - moodle_profiling_runs.xml: Metadata about the information
 513   *         exported. Contains some header information (version and
 514   *         release of moodle, database, git hash - if available, date
 515   *         of export...) and a list of all the runids included in the
 516   *         export.
 517   *    - runid.xml: One file per each run detailed in the main file,
 518   *        containing the raw dump of the given runid in the profiling table.
 519   *
 520   * Possible improvement: Start storing some extra information in the
 521   * profiling table for each run (moodle version, database, git hash...).
 522   *
 523   * @param array $runids list of runids to be exported.
 524   * @param string $file filesystem fullpath to destination .mpr file.
 525   * @return boolean the mpr file has been successfully exported (true) or no (false).
 526   */
 527  function profiling_export_runs(array $runids, $file) {
 528      global $CFG, $DB;
 529  
 530      // Verify we have passed proper runids.
 531      if (empty($runids)) {
 532          return false;
 533      }
 534  
 535      // Verify all the passed runids do exist.
 536      list ($insql, $inparams) = $DB->get_in_or_equal($runids);
 537      $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
 538      if ($reccount != count($runids)) {
 539          return false;
 540      }
 541  
 542      // Verify the $file path is writeable.
 543      $base = dirname($file);
 544      if (!is_writable($base)) {
 545          return false;
 546      }
 547  
 548      // Create temp directory where the temp information will be generated.
 549      $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
 550      mkdir($tmpdir);
 551  
 552      // Generate the xml contents in the temp directory.
 553      $status = profiling_export_generate($runids, $tmpdir);
 554  
 555      // Package (zip) all the information into the final .mpr file.
 556      if ($status) {
 557          $status = profiling_export_package($file, $tmpdir);
 558      }
 559  
 560      // Process finished ok, clean and return.
 561      fulldelete($tmpdir);
 562      return $status;
 563  }
 564  
 565  /**
 566   * Import a .mpr (moodle profile runs) file into moodle.
 567   *
 568   * See {@link profiling_export_runs()} for more details about the
 569   * implementation of .mpr files.
 570   *
 571   * @param string $file filesystem fullpath to target .mpr file.
 572   * @param string $commentprefix prefix to add to the comments of all the imported runs.
 573   * @return boolean the mpr file has been successfully imported (true) or no (false).
 574   */
 575  function profiling_import_runs($file, $commentprefix = '') {
 576      global $DB;
 577  
 578      // Any problem with the file or its directory, abort.
 579      if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
 580          return false;
 581      }
 582  
 583      // Unzip the file into temp directory.
 584      $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
 585      $fp = get_file_packer('application/vnd.moodle.profiling');
 586      $status = $fp->extract_to_pathname($file, $tmpdir);
 587  
 588      // Look for master file and verify its format.
 589      if ($status) {
 590          $mfile = $tmpdir . '/moodle_profiling_runs.xml';
 591          if (!file_exists($mfile) or !is_readable($mfile)) {
 592              $status = false;
 593          } else {
 594              $mdom = new DOMDocument();
 595              if (!$mdom->load($mfile)) {
 596                  $status = false;
 597              } else {
 598                  $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
 599              }
 600          }
 601      }
 602  
 603      // Verify all detail files exist and verify their format.
 604      if ($status) {
 605          $runs = $mdom->getElementsByTagName('run');
 606          foreach ($runs as $run) {
 607              $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);
 608              if (!file_exists($rfile) or !is_readable($rfile)) {
 609                  $status = false;
 610              } else {
 611                  $rdom = new DOMDocument();
 612                  if (!$rdom->load($rfile)) {
 613                      $status = false;
 614                  } else {
 615                      $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
 616                  }
 617              }
 618          }
 619      }
 620  
 621      // Everything looks ok, let's import all the runs.
 622      if ($status) {
 623          reset($runs);
 624          foreach ($runs as $run) {
 625              $rfile = $tmpdir . '/' . $run->getAttribute('ref');
 626              $rdom = new DOMDocument();
 627              $rdom->load($rfile);
 628              $runarr = array();
 629              $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);
 630              $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);
 631              $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);
 632              $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);
 633              $runarr['timecreated'] = time(); // Now.
 634              $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);
 635              $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);
 636              $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);
 637              $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);
 638              $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);
 639              // If the runid does not exist, insert it.
 640              if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
 641                  if (@gzuncompress(base64_decode($runarr['data'])) === false) {
 642                      $runarr['data'] = base64_encode(gzcompress(base64_decode($runarr['data'])));
 643                  }
 644                  $DB->insert_record('profiling', $runarr);
 645              } else {
 646                  return false;
 647              }
 648          }
 649      }
 650  
 651      // Clean the temp directory used for import.
 652      remove_dir($tmpdir);
 653  
 654      return $status;
 655  }
 656  
 657  /**
 658   * Generate the mpr contents (xml files) in the temporal directory.
 659   *
 660   * @param array $runids list of runids to be generated.
 661   * @param string $tmpdir filesystem fullpath of tmp generation.
 662   * @return boolean the mpr contents have been generated (true) or no (false).
 663   */
 664  function profiling_export_generate(array $runids, $tmpdir) {
 665      global $CFG, $DB;
 666  
 667      if (empty($CFG->release) || empty($CFG->version)) {
 668          // Some scripts may not have included version.php.
 669          include($CFG->dirroot.'/version.php');
 670          $CFG->release = $release;
 671          $CFG->version = $version;
 672      }
 673  
 674      // Calculate the header information to be sent to moodle_profiling_runs.xml.
 675      $release = $CFG->release;
 676      $version = $CFG->version;
 677      $dbtype = $CFG->dbtype;
 678      $githash = phpunit_util::get_git_hash();
 679      $date = time();
 680  
 681      // Create the xml output and writer for the main file.
 682      $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
 683      $mainxw = new xml_writer($mainxo);
 684  
 685      // Output begins.
 686      $mainxw->start();
 687      $mainxw->begin_tag('moodle_profiling_runs');
 688  
 689      // Send header information.
 690      $mainxw->begin_tag('info');
 691      $mainxw->full_tag('release', $release);
 692      $mainxw->full_tag('version', $version);
 693      $mainxw->full_tag('dbtype', $dbtype);
 694      if ($githash) {
 695          $mainxw->full_tag('githash', $githash);
 696      }
 697      $mainxw->full_tag('date', $date);
 698      $mainxw->end_tag('info');
 699  
 700      // Send information about runs.
 701      $mainxw->begin_tag('runs');
 702      foreach ($runids as $runid) {
 703          // Get the run information from DB.
 704          $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);
 705          $attributes = array(
 706                  'id' => $run->id,
 707                  'ref' => $run->runid . '.xml');
 708          $mainxw->full_tag('run', null, $attributes);
 709          // Create the individual run file.
 710          $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
 711          $runxw = new xml_writer($runxo);
 712          $runxw->start();
 713          $runxw->begin_tag('moodle_profiling_run');
 714          $runxw->full_tag('id', $run->id);
 715          $runxw->full_tag('runid', $run->runid);
 716          $runxw->full_tag('url', $run->url);
 717          $runxw->full_tag('runreference', $run->runreference);
 718          $runxw->full_tag('runcomment', $run->runcomment);
 719          $runxw->full_tag('timecreated', $run->timecreated);
 720          $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);
 721          $runxw->full_tag('totalcputime', $run->totalcputime);
 722          $runxw->full_tag('totalcalls', $run->totalcalls);
 723          $runxw->full_tag('totalmemory', $run->totalmemory);
 724          $runxw->full_tag('data', $run->data);
 725          $runxw->end_tag('moodle_profiling_run');
 726          $runxw->stop();
 727      }
 728      $mainxw->end_tag('runs');
 729      $mainxw->end_tag('moodle_profiling_runs');
 730      $mainxw->stop();
 731  
 732      return true;
 733  }
 734  
 735  /**
 736   * Package (zip) the mpr contents (xml files) in the final location.
 737   *
 738   * @param string $file filesystem fullpath to destination .mpr file.
 739   * @param string $tmpdir filesystem fullpath of tmp generation.
 740   * @return boolean the mpr contents have been generated (true) or no (false).
 741   */
 742  function profiling_export_package($file, $tmpdir) {
 743      // Get the list of files in $tmpdir.
 744      $filestemp = get_directory_list($tmpdir, '', false, true, true);
 745      $files = array();
 746  
 747      // Add zip paths and fs paths to all them.
 748      foreach ($filestemp as $filetemp) {
 749          $files[$filetemp] = $tmpdir . '/' . $filetemp;
 750      }
 751  
 752      // Get the zip_packer.
 753      $zippacker = get_file_packer('application/zip');
 754  
 755      // Generate the packaged file.
 756      $zippacker->archive_to_pathname($files, $file);
 757  
 758      return true;
 759  }
 760  
 761  /**
 762   * Return the xml schema for the main import file.
 763   *
 764   * @return string
 765   *
 766   */
 767  function profiling_get_import_main_schema() {
 768      $schema = <<<EOS
 769  <?xml version="1.0" encoding="UTF-8"?>
 770  <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
 771    <xs:element name="moodle_profiling_runs">
 772      <xs:complexType>
 773        <xs:sequence>
 774          <xs:element ref="info"/>
 775          <xs:element ref="runs"/>
 776        </xs:sequence>
 777      </xs:complexType>
 778    </xs:element>
 779    <xs:element name="info">
 780      <xs:complexType>
 781        <xs:sequence>
 782          <xs:element type="xs:string" name="release"/>
 783          <xs:element type="xs:decimal" name="version"/>
 784          <xs:element type="xs:string" name="dbtype"/>
 785          <xs:element type="xs:string" minOccurs="0" name="githash"/>
 786          <xs:element type="xs:int" name="date"/>
 787        </xs:sequence>
 788      </xs:complexType>
 789    </xs:element>
 790    <xs:element name="runs">
 791      <xs:complexType>
 792        <xs:sequence>
 793          <xs:element maxOccurs="unbounded" ref="run"/>
 794        </xs:sequence>
 795      </xs:complexType>
 796    </xs:element>
 797    <xs:element name="run">
 798      <xs:complexType>
 799        <xs:attribute type="xs:int" name="id"/>
 800        <xs:attribute type="xs:string" name="ref"/>
 801      </xs:complexType>
 802    </xs:element>
 803  </xs:schema>
 804  EOS;
 805      return $schema;
 806  }
 807  
 808  /**
 809   * Return the xml schema for each individual run import file.
 810   *
 811   * @return string
 812   *
 813   */
 814  function profiling_get_import_run_schema() {
 815      $schema = <<<EOS
 816  <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
 817    <xs:element name="moodle_profiling_run">
 818      <xs:complexType>
 819        <xs:sequence>
 820          <xs:element type="xs:int" name="id"/>
 821          <xs:element type="xs:string" name="runid"/>
 822          <xs:element type="xs:string" name="url"/>
 823          <xs:element type="xs:int" name="runreference"/>
 824          <xs:element type="xs:string" name="runcomment"/>
 825          <xs:element type="xs:int" name="timecreated"/>
 826          <xs:element type="xs:integer" name="totalexecutiontime"/>
 827          <xs:element type="xs:integer" name="totalcputime"/>
 828          <xs:element type="xs:integer" name="totalcalls"/>
 829          <xs:element type="xs:integer" name="totalmemory"/>
 830          <xs:element type="xs:string" name="data"/>
 831        </xs:sequence>
 832      </xs:complexType>
 833    </xs:element>
 834  </xs:schema>
 835  EOS;
 836      return $schema;
 837  }
 838  /**
 839   * Custom implementation of iXHProfRuns
 840   *
 841   * This class is one implementation of the iXHProfRuns interface, in charge
 842   * of storing and retrieve profiling run data to/from DB (profiling table)
 843   *
 844   * The interface only defines two methods to be defined: get_run() and
 845   * save_run() we'll be implementing some more in order to keep all the
 846   * rest of information in our runs properly handled.
 847   */
 848  class moodle_xhprofrun implements iXHProfRuns {
 849  
 850      protected $runid = null;
 851      protected $url = null;
 852      protected $totalexecutiontime = 0;
 853      protected $totalcputime = 0;
 854      protected $totalcalls = 0;
 855      protected $totalmemory = 0;
 856      protected $timecreated = 0;
 857  
 858      public function __construct() {
 859          $this->timecreated = time();
 860      }
 861  
 862      /**
 863       * Given one runid and one type, return the run data
 864       * and some extra info in run_desc from DB
 865       *
 866       * Note that $type is completely ignored
 867       */
 868      public function get_run($run_id, $type, &$run_desc) {
 869          global $DB;
 870  
 871          $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
 872  
 873          $this->runid = $rec->runid;
 874          $this->url = $rec->url;
 875          $this->totalexecutiontime = $rec->totalexecutiontime;
 876          $this->totalcputime = $rec->totalcputime;
 877          $this->totalcalls = $rec->totalcalls;
 878          $this->totalmemory = $rec->totalmemory;
 879          $this->timecreated = $rec->timecreated;
 880  
 881          $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
 882  
 883          // Handle historical runs that aren't compressed.
 884          if (@gzuncompress(base64_decode($rec->data)) === false) {
 885              return unserialize(base64_decode($rec->data));
 886          } else {
 887              return unserialize(gzuncompress(base64_decode($rec->data)));
 888          }
 889      }
 890  
 891      /**
 892       * Given some run data, one type and, optionally, one runid
 893       * store the information in DB
 894       *
 895       * Note that $type is completely ignored
 896       */
 897      public function save_run($xhprof_data, $type, $run_id = null) {
 898          global $DB, $CFG;
 899  
 900          if (is_null($this->url)) {
 901              xhprof_error("Warning: You must use the prepare_run() method before saving it");
 902          }
 903  
 904          // Calculate runid if needed
 905          $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
 906  
 907          // Calculate totals
 908          $this->totalexecutiontime = $xhprof_data['main()']['wt'];
 909          $this->totalcputime = $xhprof_data['main()']['cpu'];
 910          $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
 911          $this->totalmemory = $xhprof_data['main()']['mu'];
 912  
 913          // Prepare data
 914          $rec = new stdClass();
 915          $rec->runid = $this->runid;
 916          $rec->url = $this->url;
 917          $rec->totalexecutiontime = $this->totalexecutiontime;
 918          $rec->totalcputime = $this->totalcputime;
 919          $rec->totalcalls = $this->totalcalls;
 920          $rec->totalmemory = $this->totalmemory;
 921          $rec->timecreated = $this->timecreated;
 922  
 923          // Send to database with compressed and endoded data.
 924          if (empty($CFG->disableprofilingtodatabase)) {
 925              $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
 926              $DB->insert_record('profiling', $rec);
 927          }
 928  
 929          // Send raw data to plugins.
 930          $rec->data = $xhprof_data;
 931  
 932          // Allow a plugin to take the trace data and process it.
 933          if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
 934              foreach ($pluginsfunction as $plugintype => $plugins) {
 935                  foreach ($plugins as $pluginfunction) {
 936                      $pluginfunction($rec);
 937                  }
 938              }
 939          }
 940  
 941          if (PHPUNIT_TEST) {
 942              // Calculate export variables.
 943              $tempdir = 'profiling';
 944              make_temp_directory($tempdir);
 945              $runids = array($this->runid);
 946              $filename = $this->runid . '.mpr';
 947              $filepath = $CFG->tempdir . '/' . $tempdir . '/' . $filename;
 948  
 949              // Generate the mpr file and send it.
 950              if (profiling_export_runs($runids, $filepath)) {
 951                  fprintf(STDERR, "Profiling data saved to: ".$filepath."\n");
 952              }
 953          }
 954  
 955          return $this->runid;
 956      }
 957  
 958      public function prepare_run($url) {
 959          $this->url = $url;
 960      }
 961  
 962      // Private API starts here
 963  
 964      protected function sum_calls($sum, $data) {
 965          return $sum + $data['ct'];
 966      }
 967  }
 968  
 969  /**
 970   * Simple subclass of {@link table_sql} that provides
 971   * some custom formatters for various columns, in order
 972   * to make the main profiles list nicer
 973   */
 974  class xhprof_table_sql extends table_sql {
 975  
 976      protected $listurlmode = false;
 977  
 978      /**
 979       * Get row classes to be applied based on row contents
 980       */
 981      function get_row_class($row) {
 982          return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
 983      }
 984  
 985      /**
 986       * Define it the table is in listurlmode or not, output will
 987       * be different based on that
 988       */
 989      function set_listurlmode($listurlmode) {
 990          $this->listurlmode = $listurlmode;
 991      }
 992  
 993      /**
 994       * Format URL, so it points to last run for that url
 995       */
 996      protected function col_url($row) {
 997          global $OUTPUT;
 998  
 999          // Build the link to latest run for the script
1000          $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
1001          $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
1002  
1003          // Decide, based on $this->listurlmode which actions to show
1004          if ($this->listurlmode) {
1005              $detailsaction = '';
1006          } else {
1007              // Build link icon to script details (pix + url + actionlink)
1008              $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
1009              $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
1010              $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
1011          }
1012  
1013          return $scriptaction . '&nbsp;' . $detailsaction;
1014      }
1015  
1016      /**
1017       * Format profiling date, human and pointing to run
1018       */
1019      protected function col_timecreated($row) {
1020          global $OUTPUT;
1021          $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
1022          $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
1023          return $OUTPUT->action_link($url, $fdate);
1024      }
1025  
1026      /**
1027       * Format execution time
1028       */
1029      protected function col_totalexecutiontime($row) {
1030          return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
1031      }
1032  
1033      /**
1034       * Format cpu time
1035       */
1036      protected function col_totalcputime($row) {
1037          return format_float($row->totalcputime / 1000, 3) . ' ms';
1038      }
1039  
1040      /**
1041       * Format memory
1042       */
1043      protected function col_totalmemory($row) {
1044          return format_float($row->totalmemory / 1024, 3) . ' KB';
1045      }
1046  }