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