Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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