Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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 . '&amp;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/>&nbsp;".
349
                     "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
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 . '&amp;runid2=' . $reference->runid . '&amp;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 . '&amp;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 .= '&nbsp;<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 ? '&Delta;' : '&asymp;';
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 . '&nbsp;' . $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
}