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 . '&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/> ". 349 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/> ". 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 . '&runid2=' . $reference->runid . '&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 . '&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 .= ' <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 ? 'Δ' : '≈'; 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 . ' ' . $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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body