Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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      public function __construct() {
     863          $this->timecreated = time();
     864      }
     865  
     866      /**
     867       * Given one runid and one type, return the run data
     868       * and some extra info in run_desc from DB
     869       *
     870       * Note that $type is completely ignored
     871       */
     872      public function get_run($run_id, $type, &$run_desc) {
     873          global $DB;
     874  
     875          $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
     876  
     877          $this->runid = $rec->runid;
     878          $this->url = $rec->url;
     879          $this->totalexecutiontime = $rec->totalexecutiontime;
     880          $this->totalcputime = $rec->totalcputime;
     881          $this->totalcalls = $rec->totalcalls;
     882          $this->totalmemory = $rec->totalmemory;
     883          $this->timecreated = $rec->timecreated;
     884  
     885          $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
     886  
     887          // Handle historical runs that aren't compressed.
     888          if (@gzuncompress(base64_decode($rec->data)) === false) {
     889              return unserialize(base64_decode($rec->data));
     890          } else {
     891              return unserialize(gzuncompress(base64_decode($rec->data)));
     892          }
     893      }
     894  
     895      /**
     896       * Given some run data, one type and, optionally, one runid
     897       * store the information in DB
     898       *
     899       * Note that $type is completely ignored
     900       */
     901      public function save_run($xhprof_data, $type, $run_id = null) {
     902          global $DB, $CFG;
     903  
     904          if (is_null($this->url)) {
     905              xhprof_error("Warning: You must use the prepare_run() method before saving it");
     906          }
     907  
     908          // Calculate runid if needed
     909          $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
     910  
     911          // Calculate totals
     912          $this->totalexecutiontime = $xhprof_data['main()']['wt'];
     913          $this->totalcputime = $xhprof_data['main()']['cpu'];
     914          $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
     915          $this->totalmemory = $xhprof_data['main()']['mu'];
     916  
     917          // Prepare data
     918          $rec = new stdClass();
     919          $rec->runid = $this->runid;
     920          $rec->url = $this->url;
     921          $rec->totalexecutiontime = $this->totalexecutiontime;
     922          $rec->totalcputime = $this->totalcputime;
     923          $rec->totalcalls = $this->totalcalls;
     924          $rec->totalmemory = $this->totalmemory;
     925          $rec->timecreated = $this->timecreated;
     926  
     927          // Send to database with compressed and endoded data.
     928          if (empty($CFG->disableprofilingtodatabase)) {
     929              $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
     930              $DB->insert_record('profiling', $rec);
     931          }
     932  
     933          // Send raw data to plugins.
     934          $rec->data = $xhprof_data;
     935  
     936          // Allow a plugin to take the trace data and process it.
     937          if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
     938              foreach ($pluginsfunction as $plugintype => $plugins) {
     939                  foreach ($plugins as $pluginfunction) {
     940                      $pluginfunction($rec);
     941                  }
     942              }
     943          }
     944  
     945          if (PHPUNIT_TEST) {
     946              // Calculate export variables.
     947              $tempdir = 'profiling';
     948              make_temp_directory($tempdir);
     949              $runids = array($this->runid);
     950              $filename = $this->runid . '.mpr';
     951              $filepath = $CFG->tempdir . '/' . $tempdir . '/' . $filename;
     952  
     953              // Generate the mpr file and send it.
     954              if (profiling_export_runs($runids, $filepath)) {
     955                  fprintf(STDERR, "Profiling data saved to: ".$filepath."\n");
     956              }
     957          }
     958  
     959          return $this->runid;
     960      }
     961  
     962      public function prepare_run($url) {
     963          $this->url = $url;
     964      }
     965  
     966      // Private API starts here
     967  
     968      protected function sum_calls($sum, $data) {
     969          return $sum + $data['ct'];
     970      }
     971  }
     972  
     973  /**
     974   * Simple subclass of {@link table_sql} that provides
     975   * some custom formatters for various columns, in order
     976   * to make the main profiles list nicer
     977   */
     978  class xhprof_table_sql extends table_sql {
     979  
     980      protected $listurlmode = false;
     981  
     982      /**
     983       * Get row classes to be applied based on row contents
     984       */
     985      function get_row_class($row) {
     986          return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
     987      }
     988  
     989      /**
     990       * Define it the table is in listurlmode or not, output will
     991       * be different based on that
     992       */
     993      function set_listurlmode($listurlmode) {
     994          $this->listurlmode = $listurlmode;
     995      }
     996  
     997      /**
     998       * Format URL, so it points to last run for that url
     999       */
    1000      protected function col_url($row) {
    1001          global $OUTPUT;
    1002  
    1003          // Build the link to latest run for the script
    1004          $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
    1005          $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
    1006  
    1007          // Decide, based on $this->listurlmode which actions to show
    1008          if ($this->listurlmode) {
    1009              $detailsaction = '';
    1010          } else {
    1011              // Build link icon to script details (pix + url + actionlink)
    1012              $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
    1013              $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
    1014              $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
    1015          }
    1016  
    1017          return $scriptaction . '&nbsp;' . $detailsaction;
    1018      }
    1019  
    1020      /**
    1021       * Format profiling date, human and pointing to run
    1022       */
    1023      protected function col_timecreated($row) {
    1024          global $OUTPUT;
    1025          $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
    1026          $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
    1027          return $OUTPUT->action_link($url, $fdate);
    1028      }
    1029  
    1030      /**
    1031       * Format execution time
    1032       */
    1033      protected function col_totalexecutiontime($row) {
    1034          return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
    1035      }
    1036  
    1037      /**
    1038       * Format cpu time
    1039       */
    1040      protected function col_totalcputime($row) {
    1041          return format_float($row->totalcputime / 1000, 3) . ' ms';
    1042      }
    1043  
    1044      /**
    1045       * Format memory
    1046       */
    1047      protected function col_totalmemory($row) {
    1048          return format_float($row->totalmemory / 1024, 3) . ' KB';
    1049      }
    1050  }