Commit | Line | Data |
---|---|---|
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 | ||
24 | defined('MOODLE_INTERNAL') || die(); | |
25 | ||
e36a3e36 | 26 | // Need some stuff from xhprof. |
6af80cae EL |
27 | require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_lib.php'); |
28 | require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php'); | |
e36a3e36 EL |
29 | // Need some stuff from moodle. |
30 | require_once($CFG->libdir . '/tablelib.php'); | |
31 | require_once($CFG->libdir . '/setuplib.php'); | |
8f7dcb34 | 32 | require_once($CFG->libdir . '/filelib.php'); |
e36a3e36 EL |
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'); | |
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 | */ | |
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 | * Start profiling observing all the configuration | |
68 | */ | |
69 | function 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 | */ | |
164 | function 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 | ||
213 | function 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 | */ | |
239 | function 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 |
257 | function 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 . '&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 | */ | |
282 | function 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/> ". | |
294 | "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/> ". | |
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 . '&runid2=' . $reference->runid . '&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 . '&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 | ||
347 | function 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 | */ | |
402 | function profiling_list_controls($listurl) { | |
2e075396 | 403 | global $CFG; |
6af80cae | 404 | |
2e075396 EL |
405 | $output = '<p class="centerpara buttons">'; |
406 | $output .= ' <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 | */ | |
416 | function 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 | */ | |
436 | function 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 ? 'Δ' : '≈'; | |
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 | */ |
476 | function 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 |
524 | function 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 | */ | |
613 | function 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 | */ | |
691 | function 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 | */ | |
716 | function 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> | |
753 | EOS; | |
754 | return $schema; | |
755 | } | |
756 | ||
757 | /** | |
758 | * Return the xml schema for each individual run import file. | |
759 | * | |
760 | * @return string | |
761 | * | |
762 | */ | |
763 | function 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> | |
784 | EOS; | |
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 | */ | |
797 | class 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 | */ | |
908 | class 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 . ' ' . $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 | } |