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