1 |
efrain |
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 |
}
|