<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * @package    core
 * @subpackage profiling
 * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

// Need some stuff from xhprof.
require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_lib.php');
require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php');
// Need some stuff from moodle.
require_once($CFG->libdir . '/tablelib.php');
require_once($CFG->libdir . '/setuplib.php');
require_once($CFG->libdir . '/filelib.php');
require_once($CFG->libdir . '/phpunit/classes/util.php');
require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');

// TODO: Change the implementation below to proper profiling class.

/**
 * Returns if profiling is running, optionally setting it
 */
function profiling_is_running($value = null) {
    static $running = null;

    if (!is_null($value)) {
        $running = (bool)$value;
    }

    return $running;
}

/**
 * Returns if profiling has been saved, optionally setting it
 */
function profiling_is_saved($value = null) {
    static $saved = null;

    if (!is_null($value)) {
        $saved = (bool)$value;
    }

    return $saved;
}

/**
 * Whether PHP profiling is available.
 *
 * This check ensures that one of the available PHP Profiling extensions is available.
 *
 * @return  bool
 */
function profiling_available() {
    $hasextension = extension_loaded('tideways_xhprof');
    $hasextension = $hasextension || extension_loaded('tideways');
    $hasextension = $hasextension || extension_loaded('xhprof');

    return $hasextension;
}

/**
 * Start profiling observing all the configuration
 */
function profiling_start() {
    global $CFG, $SESSION, $SCRIPT;

    // If profiling isn't available, nothing to start
    if (!profiling_available()) {
        return false;
    }

    // If profiling isn't enabled, nothing to start
    if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
        return false;
    }

    // If profiling is already running or saved, nothing to start
    if (profiling_is_running() || profiling_is_saved()) {
        return false;
    }

    // Set script (from global if available, else our own)
    $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();

    // Get PGC variables
    $profileme      = profiling_get_flag('PROFILEME')       && !empty($CFG->profilingallowme);
    $dontprofileme  = profiling_get_flag('DONTPROFILEME')   && !empty($CFG->profilingallowme);
    $profileall     = profiling_get_flag('PROFILEALL')      && !empty($CFG->profilingallowall);
    $profileallstop = profiling_get_flag('PROFILEALLSTOP')  && !empty($CFG->profilingallowall);

    // DONTPROFILEME detected, nothing to start
    if ($dontprofileme) {
        return false;
    }

    // PROFILEALLSTOP detected, clean the mark in seesion and continue
    if ($profileallstop && !empty($SESSION)) {
        unset($SESSION->profileall);
    }

    // PROFILEALL detected, set the mark in session and continue
    if ($profileall && !empty($SESSION)) {
        $SESSION->profileall = true;

    // SESSION->profileall detected, set $profileall
    } else if (!empty($SESSION->profileall)) {
        $profileall = true;
    }

    // Evaluate automatic (random) profiling if necessary
    $profileauto = false;
    if (!empty($CFG->profilingautofrec)) {
        $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
    }

    // Profile potentially slow pages.
    $profileslow = false;
    if (!empty($CFG->profilingslow) && !CLI_SCRIPT) {
        $profileslow = true;
    }

    // See if the $script matches any of the included patterns.
    $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
    $profileincluded = profiling_string_matches($script, $included);

    // See if the $script matches any of the excluded patterns
    $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
    $profileexcluded = profiling_string_matches($script, $excluded);

    // Decide if profile auto must happen (observe matchings)
    $profileauto = $profileauto && $profileincluded && !$profileexcluded;

    // Decide if profile by match must happen (only if profileauto is disabled)
    $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);

    // Decide if slow profile has been excluded.
    $profileslow = $profileslow && !$profileexcluded;

    // If not auto, me, all, match have been detected, nothing to do.
    if (!$profileauto && !$profileme && !$profileall && !$profilematch && !$profileslow) {
        return false;
    }

    // If we have only been triggered by a *potentially* slow page then remember this for later.
    if ((!$profileauto && !$profileme && !$profileall && !$profilematch) && $profileslow) {
        $CFG->profilepotentialslowpage = microtime(true); // Neither $PAGE or $SESSION are guaranteed here.
    }

    // Arrived here, the script is going to be profiled, let's do it
    $ignore = array('call_user_func', 'call_user_func_array');
    if (extension_loaded('tideways_xhprof')) {
        tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY);
    } else if (extension_loaded('tideways')) {
        tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
    } else {
        xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
    }
    profiling_is_running(true);

    // Started, return true
    return true;
}

/**
 * Check for profiling flags in all possible places
 * @param string $flag name
 * @return boolean
 */
function profiling_get_flag($flag) {
    return !empty(getenv($flag)) ||
        isset($_COOKIE[$flag]) ||
        isset($_POST[$flag]) ||
        isset($_GET[$flag]);
}

/**
 * Stop profiling, gathering results and storing them
 */
function profiling_stop() {
    global $CFG, $DB, $SCRIPT;

    // If profiling isn't available, nothing to stop
    if (!profiling_available()) {
        return false;
    }

    // If profiling isn't enabled, nothing to stop
    if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
        return false;
    }

    // If profiling is not running or is already saved, nothing to stop
    if (!profiling_is_running() || profiling_is_saved()) {
        return false;
    }

    // Set script (from global if available, else our own)
    $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();

    // Arrived here, profiling is running, stop and save everything
    profiling_is_running(false);
    if (extension_loaded('tideways_xhprof')) {
        $data = tideways_xhprof_disable();
    } else if (extension_loaded('tideways')) {
        $data = tideways_disable();
    } else {
        $data = xhprof_disable();
    }

    // We only save the run after ensuring the DB table exists
    // (this prevents problems with profiling runs enabled in
    // config.php before Moodle is installed. Rare but...
    $tables = $DB->get_tables();
    if (!in_array('profiling', $tables)) {
        return false;
    }

    // If we only profiled because it was potentially slow then...
    if (!empty($CFG->profilepotentialslowpage)) {
        $duration = microtime(true) - $CFG->profilepotentialslowpage;
        if ($duration < $CFG->profilingslow) {
            // Wasn't slow enough.
            return false;
        }

        $sql = "SELECT max(totalexecutiontime)
                  FROM {profiling}
                 WHERE url = ?";
        $slowest = $DB->get_field_sql($sql, array($script));
        if (!empty($slowest) && $duration * 1000000 < $slowest) {
            // Already have a worse profile stored.
            return false;
        }
    }

    $run = new moodle_xhprofrun();
    $run->prepare_run($script);
    $runid = $run->save_run($data, null);
    profiling_is_saved(true);

    // Prune old runs
    profiling_prune_old_runs($runid);

    // Finished, return true
    return true;
}

function profiling_prune_old_runs($exception = 0) {
    global $CFG, $DB;

    // Setting to 0 = no prune
    if (empty($CFG->profilinglifetime)) {
        return;
    }

    $cuttime = time() - ($CFG->profilinglifetime * 60);
    $params = array('cuttime' => $cuttime, 'exception' => $exception);

    $DB->delete_records_select('profiling', 'runreference = 0 AND
                                             timecreated < :cuttime AND
                                             runid != :exception', $params);
}

/**
 * Returns the path to the php script being requested
 *
 * Note this function is a partial copy of initialise_fullme() and
 * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
 * friends. To be used by early profiling runs in situations where
 * $SCRIPT isn't defined yet
 *
 * @return string absolute path (wwwroot based) of the script being executed
 */
function profiling_get_script() {
    global $CFG;

    $wwwroot = parse_url($CFG->wwwroot);

    if (!isset($wwwroot['path'])) {
        $wwwroot['path'] = '';
    }
    $wwwroot['path'] .= '/';

    $path = $_SERVER['SCRIPT_NAME'];

    if (strpos($path, $wwwroot['path']) === 0) {
        return substr($path, strlen($wwwroot['path']) - 1);
    }
    return '';
}

function profiling_urls($report, $runid, $runid2 = null) {
    global $CFG;

    $url = '';
    switch ($report) {
        case 'run':
            $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
            break;
        case 'diff':
            $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
            break;
        case 'graph':
            $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
            break;
    }
    return $url;
}

/**
 * Generate the output to print a profiling run including further actions you can then take.
 *
 * @param object $run The profiling run object we are going to display.
 * @param array $prevreferences A list of run objects to list as comparison targets.
 * @return string The output to display on the screen for this run.
 */
function profiling_print_run($run, $prevreferences = null) {
    global $CFG, $OUTPUT;

    $output = '';

    // Prepare the runreference/runcomment form
    $checked = $run->runreference ? ' checked=checked' : '';
    $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
                     "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
                     "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
                     "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
                     "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
                     "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
                     "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
                     "</form>";

    $table = new html_table();
    $table->align = array('right', 'left');
    $table->tablealign = 'center';
    $table->attributes['class'] = 'profilingruntable';
    $table->colclasses = array('label', 'value');
    $table->data = array(
       array(get_string('runid', 'tool_profiling'), $run->runid),
       array(get_string('url'), $run->url),
       array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
       array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
       array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
       array(get_string('calls', 'tool_profiling'), $run->totalcalls),
       array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
       array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
    $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
    // Add link to details
    $strviewdetails = get_string('viewdetails', 'tool_profiling');
    $url = profiling_urls('run', $run->runid);
    $output .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
                                'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');

    // If there are previous run(s) marked as reference, add link to diff.
    if ($prevreferences) {
        $table = new html_table();
        $table->align = array('left', 'left');
        $table->head = array(get_string('date'), get_string('runid', 'tool_profiling'), get_string('comment', 'tool_profiling'));
        $table->tablealign = 'center';
        $table->attributes['class'] = 'flexible generaltable generalbox';
        $table->colclasses = array('value', 'value', 'value');
        $table->data = array();

        $output .= $OUTPUT->heading(get_string('viewdiff', 'tool_profiling'), 3, 'main profilinglink');

        foreach ($prevreferences as $reference) {
            $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $reference->runid . '&amp;listurl=' . urlencode($run->url);
            $row = array(userdate($reference->timecreated), '<a href="' . $url . '" title="">'.$reference->runid.'</a>', $reference->runcomment);
            $table->data[] = $row;
        }
        $output .= $OUTPUT->box(html_writer::table($table), 'profilingrunbox', 'profiling_diffs');

    }
    // Add link to export this run.
    $strexport = get_string('exportthis', 'tool_profiling');
    $url = 'export.php?runid=' . $run->runid . '&amp;listurl=' . urlencode($run->url);
    $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strexport . '</a>', 3, 'main profilinglink');

    return $output;
}

function profiling_print_rundiff($run1, $run2) {
    global $CFG, $OUTPUT;

    $output = '';

    // Prepare the reference/comment information
    $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
                      ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
    $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
                      ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');

    // Calculate global differences
    $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
    $diffcputime       = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
    $diffcalls         = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
    $diffmemory        = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);

    $table = new html_table();
    $table->align = array('right', 'left', 'left', 'left');
    $table->tablealign = 'center';
    $table->attributes['class'] = 'profilingruntable';
    $table->colclasses = array('label', 'value1', 'value2');
    $table->data = array(
       array(get_string('runid', 'tool_profiling'),
           '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
           '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
       array(get_string('url'), $run1->url, $run2->url),
       array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
           userdate($run2->timecreated, '%d %B %Y, %H:%M')),
       array(get_string('executiontime', 'tool_profiling'),
           format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
           format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
       array(get_string('cputime', 'tool_profiling'),
           format_float($run1->totalcputime / 1000, 3) . ' ms',
           format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
       array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
       array(get_string('memory', 'tool_profiling'),
           format_float($run1->totalmemory / 1024, 0) . ' KB',
           format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
       array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
    $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary');
    // Add link to details
    $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
    $url = profiling_urls('diff', $run1->runid, $run2->runid);
    //$url =  $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
    $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
                              'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
    return $output;
}

/**
 * Helper function that returns the HTML fragment to
 * be displayed on listing mode, it includes actions
 * like deletion/export/import...
 */
function profiling_list_controls($listurl) {
    global $CFG;

    $output = '<p class="centerpara buttons">';
    $output .= '&nbsp;<a href="import.php">[' . get_string('import', 'tool_profiling') . ']</a>';
    $output .= '</p>';

    return $output;
}

/**
 * Helper function that looks for matchings of one string
 * against an array of * wildchar patterns
 */
function profiling_string_matches($string, $patterns) {
   $patterns = preg_split("/\n|,/", $patterns);
    foreach ($patterns as $pattern) {
        // Trim and prepare pattern
        $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
        // Don't process empty patterns
        if (empty($pattern)) {
            continue;
        }
        if (preg_match('~^' . $pattern . '$~', $string)) {
            return true;
        }
    }
    return false;
}

/**
 * Helper function that, given to floats, returns their numerical
 * and percentual differences, propertly formated and cssstyled
 */
function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
    $numdiff = $number2 - $number1;
    $perdiff = 0;
    if ($number1 != $number2) {
        $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
    }
    $sign      = $number2 > $number1 ? '+' : '';
    $delta     = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
    $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
    $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
    $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
    $endspan   = '</span>';
    $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
    $fperdiff = $sign . format_float($perdiff, $numdec);
    return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
}

/**
 * Export profiling runs to a .mpr (moodle profile runs) file.
 *
 * This function gets an array of profiling runs (array of runids) and
 * saves a .mpr file into destination for ulterior handling.
 *
 * Format of .mpr files:
 *   mpr files are simple zip packages containing these files:
 *     - moodle_profiling_runs.xml: Metadata about the information
 *         exported. Contains some header information (version and
 *         release of moodle, database, git hash - if available, date
 *         of export...) and a list of all the runids included in the
 *         export.
 *    - runid.xml: One file per each run detailed in the main file,
 *        containing the raw dump of the given runid in the profiling table.
 *
 * Possible improvement: Start storing some extra information in the
 * profiling table for each run (moodle version, database, git hash...).
 *
 * @param array $runids list of runids to be exported.
 * @param string $file filesystem fullpath to destination .mpr file.
 * @return boolean the mpr file has been successfully exported (true) or no (false).
 */
function profiling_export_runs(array $runids, $file) {
    global $CFG, $DB;

    // Verify we have passed proper runids.
    if (empty($runids)) {
        return false;
    }

    // Verify all the passed runids do exist.
    list ($insql, $inparams) = $DB->get_in_or_equal($runids);
    $reccount = $DB->count_records_select('profiling', 'runid ' . $insql, $inparams);
    if ($reccount != count($runids)) {
        return false;
    }

    // Verify the $file path is writeable.
    $base = dirname($file);
    if (!is_writable($base)) {
        return false;
    }

    // Create temp directory where the temp information will be generated.
    $tmpdir = $base . '/' . md5(implode($runids) . time() . random_string(20));
    mkdir($tmpdir);

    // Generate the xml contents in the temp directory.
    $status = profiling_export_generate($runids, $tmpdir);

    // Package (zip) all the information into the final .mpr file.
    if ($status) {
        $status = profiling_export_package($file, $tmpdir);
    }

    // Process finished ok, clean and return.
    fulldelete($tmpdir);
    return $status;
}

/**
 * Import a .mpr (moodle profile runs) file into moodle.
 *
 * See {@link profiling_export_runs()} for more details about the
 * implementation of .mpr files.
 *
 * @param string $file filesystem fullpath to target .mpr file.
 * @param string $commentprefix prefix to add to the comments of all the imported runs.
 * @return boolean the mpr file has been successfully imported (true) or no (false).
 */
function profiling_import_runs($file, $commentprefix = '') {
    global $DB;

    // Any problem with the file or its directory, abort.
    if (!file_exists($file) or !is_readable($file) or !is_writable(dirname($file))) {
        return false;
    }

    // Unzip the file into temp directory.
    $tmpdir = dirname($file) . '/' . time() . '_' . random_string(4);
    $fp = get_file_packer('application/vnd.moodle.profiling');
    $status = $fp->extract_to_pathname($file, $tmpdir);

    // Look for master file and verify its format.
    if ($status) {
        $mfile = $tmpdir . '/moodle_profiling_runs.xml';
        if (!file_exists($mfile) or !is_readable($mfile)) {
            $status = false;
        } else {
            $mdom = new DOMDocument();
            if (!$mdom->load($mfile)) {
                $status = false;
            } else {
                $status = @$mdom->schemaValidateSource(profiling_get_import_main_schema());
            }
        }
    }

    // Verify all detail files exist and verify their format.
    if ($status) {
        $runs = $mdom->getElementsByTagName('run');
        foreach ($runs as $run) {
            $rfile = $tmpdir . '/' . clean_param($run->getAttribute('ref'), PARAM_FILE);
            if (!file_exists($rfile) or !is_readable($rfile)) {
                $status = false;
            } else {
                $rdom = new DOMDocument();
                if (!$rdom->load($rfile)) {
                    $status = false;
                } else {
                    $status = @$rdom->schemaValidateSource(profiling_get_import_run_schema());
                }
            }
        }
    }

    // Everything looks ok, let's import all the runs.
    if ($status) {
        reset($runs);
        foreach ($runs as $run) {
            $rfile = $tmpdir . '/' . $run->getAttribute('ref');
            $rdom = new DOMDocument();
            $rdom->load($rfile);
            $runarr = array();
            $runarr['runid'] = clean_param($rdom->getElementsByTagName('runid')->item(0)->nodeValue, PARAM_ALPHANUMEXT);
            $runarr['url'] = clean_param($rdom->getElementsByTagName('url')->item(0)->nodeValue, PARAM_CLEAN);
            $runarr['runreference'] = clean_param($rdom->getElementsByTagName('runreference')->item(0)->nodeValue, PARAM_INT);
            $runarr['runcomment'] = $commentprefix . clean_param($rdom->getElementsByTagName('runcomment')->item(0)->nodeValue, PARAM_CLEAN);
            $runarr['timecreated'] = time(); // Now.
            $runarr['totalexecutiontime'] = clean_param($rdom->getElementsByTagName('totalexecutiontime')->item(0)->nodeValue, PARAM_INT);
            $runarr['totalcputime'] = clean_param($rdom->getElementsByTagName('totalcputime')->item(0)->nodeValue, PARAM_INT);
            $runarr['totalcalls'] = clean_param($rdom->getElementsByTagName('totalcalls')->item(0)->nodeValue, PARAM_INT);
            $runarr['totalmemory'] = clean_param($rdom->getElementsByTagName('totalmemory')->item(0)->nodeValue, PARAM_INT);
            $runarr['data'] = clean_param($rdom->getElementsByTagName('data')->item(0)->nodeValue, PARAM_CLEAN);
            // If the runid does not exist, insert it.
            if (!$DB->record_exists('profiling', array('runid' => $runarr['runid']))) {
                if (@gzuncompress(base64_decode($runarr['data'])) === false) {
                    $runarr['data'] = base64_encode(gzcompress(base64_decode($runarr['data'])));
                }
                $DB->insert_record('profiling', $runarr);
            } else {
                return false;
            }
        }
    }

    // Clean the temp directory used for import.
    remove_dir($tmpdir);

    return $status;
}

/**
 * Generate the mpr contents (xml files) in the temporal directory.
 *
 * @param array $runids list of runids to be generated.
 * @param string $tmpdir filesystem fullpath of tmp generation.
 * @return boolean the mpr contents have been generated (true) or no (false).
 */
function profiling_export_generate(array $runids, $tmpdir) {
    global $CFG, $DB;

    if (empty($CFG->release) || empty($CFG->version)) {
        // Some scripts may not have included version.php.
        include($CFG->dirroot.'/version.php');
        $CFG->release = $release;
        $CFG->version = $version;
    }

    // Calculate the header information to be sent to moodle_profiling_runs.xml.
    $release = $CFG->release;
    $version = $CFG->version;
    $dbtype = $CFG->dbtype;
    $githash = phpunit_util::get_git_hash();
    $date = time();

    // Create the xml output and writer for the main file.
    $mainxo = new file_xml_output($tmpdir . '/moodle_profiling_runs.xml');
    $mainxw = new xml_writer($mainxo);

    // Output begins.
    $mainxw->start();
    $mainxw->begin_tag('moodle_profiling_runs');

    // Send header information.
    $mainxw->begin_tag('info');
    $mainxw->full_tag('release', $release);
    $mainxw->full_tag('version', $version);
    $mainxw->full_tag('dbtype', $dbtype);
    if ($githash) {
        $mainxw->full_tag('githash', $githash);
    }
    $mainxw->full_tag('date', $date);
    $mainxw->end_tag('info');

    // Send information about runs.
    $mainxw->begin_tag('runs');
    foreach ($runids as $runid) {
        // Get the run information from DB.
        $run = $DB->get_record('profiling', array('runid' => $runid), '*', MUST_EXIST);
        $attributes = array(
                'id' => $run->id,
                'ref' => $run->runid . '.xml');
        $mainxw->full_tag('run', null, $attributes);
        // Create the individual run file.
        $runxo = new file_xml_output($tmpdir . '/' . $attributes['ref']);
        $runxw = new xml_writer($runxo);
        $runxw->start();
        $runxw->begin_tag('moodle_profiling_run');
        $runxw->full_tag('id', $run->id);
        $runxw->full_tag('runid', $run->runid);
        $runxw->full_tag('url', $run->url);
        $runxw->full_tag('runreference', $run->runreference);
        $runxw->full_tag('runcomment', $run->runcomment);
        $runxw->full_tag('timecreated', $run->timecreated);
        $runxw->full_tag('totalexecutiontime', $run->totalexecutiontime);
        $runxw->full_tag('totalcputime', $run->totalcputime);
        $runxw->full_tag('totalcalls', $run->totalcalls);
        $runxw->full_tag('totalmemory', $run->totalmemory);
        $runxw->full_tag('data', $run->data);
        $runxw->end_tag('moodle_profiling_run');
        $runxw->stop();
    }
    $mainxw->end_tag('runs');
    $mainxw->end_tag('moodle_profiling_runs');
    $mainxw->stop();

    return true;
}

/**
 * Package (zip) the mpr contents (xml files) in the final location.
 *
 * @param string $file filesystem fullpath to destination .mpr file.
 * @param string $tmpdir filesystem fullpath of tmp generation.
 * @return boolean the mpr contents have been generated (true) or no (false).
 */
function profiling_export_package($file, $tmpdir) {
    // Get the list of files in $tmpdir.
    $filestemp = get_directory_list($tmpdir, '', false, true, true);
    $files = array();

    // Add zip paths and fs paths to all them.
    foreach ($filestemp as $filetemp) {
        $files[$filetemp] = $tmpdir . '/' . $filetemp;
    }

    // Get the zip_packer.
    $zippacker = get_file_packer('application/zip');

    // Generate the packaged file.
    $zippacker->archive_to_pathname($files, $file);

    return true;
}

/**
 * Return the xml schema for the main import file.
 *
 * @return string
 *
 */
function profiling_get_import_main_schema() {
    $schema = <<<EOS
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
  <xs:element name="moodle_profiling_runs">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="info"/>
        <xs:element ref="runs"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="info">
    <xs:complexType>
      <xs:sequence>
        <xs:element type="xs:string" name="release"/>
        <xs:element type="xs:decimal" name="version"/>
        <xs:element type="xs:string" name="dbtype"/>
        <xs:element type="xs:string" minOccurs="0" name="githash"/>
        <xs:element type="xs:int" name="date"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="runs">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="run"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="run">
    <xs:complexType>
      <xs:attribute type="xs:int" name="id"/>
      <xs:attribute type="xs:string" name="ref"/>
    </xs:complexType>
  </xs:element>
</xs:schema>
EOS;
    return $schema;
}

/**
 * Return the xml schema for each individual run import file.
 *
 * @return string
 *
 */
function profiling_get_import_run_schema() {
    $schema = <<<EOS
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
  <xs:element name="moodle_profiling_run">
    <xs:complexType>
      <xs:sequence>
        <xs:element type="xs:int" name="id"/>
        <xs:element type="xs:string" name="runid"/>
        <xs:element type="xs:string" name="url"/>
        <xs:element type="xs:int" name="runreference"/>
        <xs:element type="xs:string" name="runcomment"/>
        <xs:element type="xs:int" name="timecreated"/>
        <xs:element type="xs:integer" name="totalexecutiontime"/>
        <xs:element type="xs:integer" name="totalcputime"/>
        <xs:element type="xs:integer" name="totalcalls"/>
        <xs:element type="xs:integer" name="totalmemory"/>
        <xs:element type="xs:string" name="data"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
EOS;
    return $schema;
}
/**
 * Custom implementation of iXHProfRuns
 *
 * This class is one implementation of the iXHProfRuns interface, in charge
 * of storing and retrieve profiling run data to/from DB (profiling table)
 *
 * The interface only defines two methods to be defined: get_run() and
 * save_run() we'll be implementing some more in order to keep all the
 * rest of information in our runs properly handled.
 */
class moodle_xhprofrun implements iXHProfRuns {

    protected $runid = null;
    protected $url = null;
    protected $totalexecutiontime = 0;
    protected $totalcputime = 0;
    protected $totalcalls = 0;
    protected $totalmemory = 0;
    protected $timecreated = 0;

    /** @var bool Decide if we want to reduce profiling data or no */
    protected bool $reducedata = false;

    public function __construct() {
        $this->timecreated = time();
    }

    /**
     * Given one runid and one type, return the run data
     * and some extra info in run_desc from DB
     *
     * Note that $type is completely ignored
     */
    public function get_run($run_id, $type, &$run_desc) {
        global $DB;

        $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);

        $this->runid = $rec->runid;
        $this->url = $rec->url;
        $this->totalexecutiontime = $rec->totalexecutiontime;
        $this->totalcputime = $rec->totalcputime;
        $this->totalcalls = $rec->totalcalls;
        $this->totalmemory = $rec->totalmemory;
        $this->timecreated = $rec->timecreated;

        $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);

        // Handle historical runs that aren't compressed.
        if (@gzuncompress(base64_decode($rec->data)) === false) {
            return unserialize(base64_decode($rec->data));
        } else {
            $info = unserialize(gzuncompress(base64_decode($rec->data)));
            if (!$this->reducedata) {
                // We want to return the full data.
                return $info;
            }

            // We want to apply some transformations here, in order to reduce
            // the information for some complex (too many levels) cases.
            return $this->reduce_run_data($info);
        }
    }

    /**
     * Given some run data, one type and, optionally, one runid
     * store the information in DB
     *
     * Note that $type is completely ignored
     */
    public function save_run($xhprof_data, $type, $run_id = null) {
        global $DB, $CFG;

        if (is_null($this->url)) {
            xhprof_error("Warning: You must use the prepare_run() method before saving it");
        }

        // Calculate runid if needed
        $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;

        // Calculate totals
        $this->totalexecutiontime = $xhprof_data['main()']['wt'];
        $this->totalcputime = $xhprof_data['main()']['cpu'];
        $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
        $this->totalmemory = $xhprof_data['main()']['mu'];

        // Prepare data
        $rec = new stdClass();
        $rec->runid = $this->runid;
        $rec->url = $this->url;
        $rec->totalexecutiontime = $this->totalexecutiontime;
        $rec->totalcputime = $this->totalcputime;
        $rec->totalcalls = $this->totalcalls;
        $rec->totalmemory = $this->totalmemory;
        $rec->timecreated = $this->timecreated;

        // Send to database with compressed and endoded data.
        if (empty($CFG->disableprofilingtodatabase)) {
            $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
            $DB->insert_record('profiling', $rec);
        }

        // Send raw data to plugins.
        $rec->data = $xhprof_data;

        // Allow a plugin to take the trace data and process it.
        if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
            foreach ($pluginsfunction as $plugintype => $plugins) {
                foreach ($plugins as $pluginfunction) {
                    $pluginfunction($rec);
                }
            }
        }

        if (PHPUNIT_TEST) {
            // Calculate export variables.
            $tempdir = 'profiling';
            make_temp_directory($tempdir);
            $runids = array($this->runid);
            $filename = $this->runid . '.mpr';
            $filepath = $CFG->tempdir . '/' . $tempdir . '/' . $filename;

            // Generate the mpr file and send it.
            if (profiling_export_runs($runids, $filepath)) {
                fprintf(STDERR, "Profiling data saved to: ".$filepath."\n");
            }
        }

        return $this->runid;
    }

    public function prepare_run($url) {
        $this->url = $url;
    }

    /**
     * Enable or disable reducing profiling data.
     *
     * @param bool $reducedata Decide if we want to reduce profiling data (true) or no (false).
     */
    public function set_reducedata(bool $reducedata): void {
        $this->reducedata = $reducedata;
    }

    // Private API starts here.

    protected function sum_calls($sum, $data) {
        return $sum + $data['ct'];
    }

    /**
     * Reduce the run data to a more manageable size.
     *
     * This removes from the run data all the entries that
     * are matching a group of regular expressions.
     *
     * The main use is to remove all the calls between "__Mustache"
     * functions, which don't provide any useful information and
     * make the call-graph too complex to be handled.
     *
     * @param array $info The xhprof run data, original array.
     * @return array The xhprof run data, reduced array.
     */
    protected function reduce_run_data(array $info): array {
        // Define which (regular expressions) we want to remove. Already escaped if needed to, please.
        $toremove = [
            '__Mustache.*==>__Mustache.*', // All __Mustache to __Mustache calls.
        ];
        // Build the regular expression to be used.
        $regexp = '/^(' . implode('|', $toremove) . ')$/';

        // Given that the keys of the array have the format "parent==>child"
        // we want to rebuild the array with the same structure but
        // topologically sorted (parents always before children).
        // Note that we do this exclusively to guarantee that the
        // second pass (see below) works properly in all cases because,
        // without it, we may need to perform N (while loop) second passes.
        $sorted = $this->xhprof_topo_sort($info);

        // To keep track of removed and remaining (child-parent) pairs.
        $removed = [];
        $remaining = [];

        // First pass, we are going to remove all the elements which
        // both parent and child are __Mustache function calls.
        foreach ($sorted as $key => $value) {
            if (!str_contains($key, '==>')) {
                $parent = 'NULL';
                $child = $key;
            } else {
                [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
            }

            if (preg_match($regexp, $key)) {
                unset($sorted[$key]);
                $removed[$child][$parent] = true;
            } else {
                $remaining[$child][$parent] = true;
            }
        }

        // Second pass, we are going to remove all the elements which
        // parent was removed by first pass and doesn't appear anymore
        // as a child of anything (aka, they have become orphaned).
        // Note, that thanks to the topological sorting, we can be sure
        // one unique pass is enough. Without it, we may need to perform
        // N (while loop) second passes.
        foreach ($sorted as $key => $value) {
            if (!str_contains($key, '==>')) {
                $parent = 'NULL';
                $child = $key;
            } else {
                [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
            }

            if (isset($removed[$parent]) && !isset($remaining[$parent])) {
                unset($sorted[$key]);
                $removed[$child][$parent] = true;
                unset($remaining[$child][$parent]);
                // If this was the last parent of this child, remove it completely from the remaining array.
                if (empty($remaining[$child])) {
                    unset($remaining[$child]);
                }
            }
        }

        // We are done, let's return the reduced array.
        return $sorted;
    }


    /**
     * Sort the xhprof run pseudo-topologically, so all parents are always before their children.
     *
     * Note that this is not a proper, complex, recursive topological sorting algorithm, returning
     * nodes that later have to be converted back to xhprof "pairs" but, instead, does the specific
     * work to get those parent==>child (2 levels only) "pairs" sorted (parents always before children).
     *
     * @param array $info The xhprof run data, original array.
     *
     * @return array The xhprof run data, sorted array.
     */
    protected function xhprof_topo_sort(array $info): array {
        $sorted = [];
        $visited = [];
        $remaining = $info;
        do {
            $newremaining = [];
            foreach ($remaining as $key => $value) {
                // If we already have visited this element, we can skip it.
                if (isset($visited[$key])) {
                    continue;
                }
                if (!str_contains($key, '==>')) {
                    // It's a root element, we can add it to the sorted array.
                    $sorted[$key] = $info[$key];
                    $visited[$key] = true;
                } else {
                    [$parent, $child] = explode('==>', $key); // TODO: Consider caching this in a property.
                    if (isset($visited[$parent])) {
                        // Parent already visited, we can add any children to the sorted array.
                        $sorted[$key] = $info[$key];
                        $visited[$child] = true;
                    } else {
                        // Cannot add this yet, we need to wait for the parent.
                        $newremaining[$key] = $value;
                    }
                }
            }
            // Protection against infinite loops.
            if (count($remaining) === count($newremaining)) {
                $remaining = []; // So we exit the do...while loop.
            } else {
                $remaining = $newremaining; // There is still work to do.
            }
        } while (count($remaining) > 0);

        // We are done, let's return the sorted array.
        return $sorted;
    }
}

/**
 * Simple subclass of {@link table_sql} that provides
 * some custom formatters for various columns, in order
 * to make the main profiles list nicer
 */
class xhprof_table_sql extends table_sql {

    protected $listurlmode = false;

    /**
     * Get row classes to be applied based on row contents
     */
    function get_row_class($row) {
        return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
    }

    /**
     * Define it the table is in listurlmode or not, output will
     * be different based on that
     */
    function set_listurlmode($listurlmode) {
        $this->listurlmode = $listurlmode;
    }

    /**
     * Format URL, so it points to last run for that url
     */
    protected function col_url($row) {
        global $OUTPUT;

        // Build the link to latest run for the script
        $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
        $scriptaction = $OUTPUT->action_link($scripturl, $row->url);

        // Decide, based on $this->listurlmode which actions to show
        if ($this->listurlmode) {
            $detailsaction = '';
        } else {
            // Build link icon to script details (pix + url + actionlink)
            $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
            $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
            $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
        }

        return $scriptaction . '&nbsp;' . $detailsaction;
    }

    /**
     * Format profiling date, human and pointing to run
     */
    protected function col_timecreated($row) {
        global $OUTPUT;
        $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
        $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
        return $OUTPUT->action_link($url, $fdate);
    }

    /**
     * Format execution time
     */
    protected function col_totalexecutiontime($row) {
        return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
    }

    /**
     * Format cpu time
     */
    protected function col_totalcputime($row) {
        return format_float($row->totalcputime / 1000, 3) . ' ms';
    }

    /**
     * Format memory
     */
    protected function col_totalmemory($row) {
        return format_float($row->totalmemory / 1024, 3) . ' KB';
    }
}
