<?php

/**
 * Codebench — A benchmarking module.
 *
 * @package    Kohana/Codebench
 * @category   Base
 * @author     Kohana Team
 * @copyright  (c) 2009 Kohana Team
 * @license    https://kohana.top/license.html
 */
abstract class Kohana_Codebench
{
    /**
     * @var  string  Some optional explanatory comments about the benchmark file.
     *               HTML allowed. URLs will be converted to links automatically.
     */
    public $description = '';

    /**
     * @var  integer  How many times to execute each method per subject.
     */
    public $loops = 1000;

    /**
     * @var  array  The subjects to supply iteratively to your benchmark methods.
     */
    public $subjects = [];

    /**
     * @var  array  Grade letters with their maximum scores. Used to color the graphs.
     */
    public $grades = [
        125 => 'A',
        150 => 'B',
        200 => 'C',
        300 => 'D',
        500 => 'E',
        'default' => 'F',
    ];

    /**
     * Constructor.
     *
     * @return  void
     */
    public function __construct()
    {
        // Set the maximum execution time
        set_time_limit(Kohana::$config->load('codebench')->max_execution_time);
    }

    /**
     * Runs Codebench on the extending class.
     *
     * @return  array  benchmark output
     */
    public function run()
    {
        // Array of all methods to loop over
        $methods = array_filter(get_class_methods($this), [$this, '_method_filter']);

        // Make sure the benchmark runs at least once,
        // also if no subject data has been provided.
        if (empty($this->subjects)) {
            $this->subjects = ['NULL' => null];
        }

        // Initialize benchmark output
        $codebench = [
            'class' => get_class($this),
            'description' => $this->description,
            'loops' => [
                'base' => (int) $this->loops,
                'total' => (int) $this->loops * count($this->subjects) * count($methods),
            ],
            'subjects' => $this->subjects,
            'benchmarks' => [],
        ];

        // Benchmark each method
        foreach ($methods as $method) {
            // Initialize benchmark output for this method
            $codebench['benchmarks'][$method] = ['time' => 0, 'memory' => 0];

            // Using Reflection because simply calling $this->$method($subject) in the loop below
            // results in buggy benchmark times correlating to the length of the method name.
            $reflection = new ReflectionMethod(get_class($this), $method);

            // Benchmark each subject on each method
            foreach ($this->subjects as $subject_key => $subject) {
                // Prerun each method/subject combo before the actual benchmark loop.
                // This way relatively expensive initial processes won't be benchmarked, e.g. autoloading.
                // At the same time we capture the return here so we don't have to do that in the loop anymore.
                $return = $reflection->invoke($this, $subject);

                // Start the timer for one subject
                $token = Profiler::start('codebench', $method . $subject_key);

                // The heavy work
                for ($i = 0; $i < $this->loops; ++$i) {
                    $reflection->invoke($this, $subject);
                }

                // Stop and read the timer
                $benchmark = Profiler::total($token);

                // Benchmark output specific to the current method and subject
                $codebench['benchmarks'][$method]['subjects'][$subject_key] = [
                    'return' => $return,
                    'time' => $benchmark[0],
                    'memory' => $benchmark[1],
                ];

                // Update method totals
                $codebench['benchmarks'][$method]['time'] += $benchmark[0];
                $codebench['benchmarks'][$method]['memory'] += $benchmark[1];
            }
        }

        // Initialize the fastest and slowest benchmarks for both methods and subjects, time and memory,
        // these values will be overwritten using min() and max() later on.
        // The 999999999 values look like a hack, I know, but they work,
        // unless your method runs for more than 31 years or consumes over 1GB of memory.
        $fastest_method = $fastest_subject = ['time' => 999999999, 'memory' => 999999999];
        $slowest_method = $slowest_subject = ['time' => 0, 'memory' => 0];

        // Find the fastest and slowest benchmarks, needed for the percentage calculations
        foreach ($methods as $method) {
            // Update the fastest and slowest method benchmarks
            $fastest_method['time'] = min($fastest_method['time'], $codebench['benchmarks'][$method]['time']);
            $fastest_method['memory'] = min($fastest_method['memory'], $codebench['benchmarks'][$method]['memory']);
            $slowest_method['time'] = max($slowest_method['time'], $codebench['benchmarks'][$method]['time']);
            $slowest_method['memory'] = max($slowest_method['memory'], $codebench['benchmarks'][$method]['memory']);

            foreach ($this->subjects as $subject_key => $subject) {
                // Update the fastest and slowest subject benchmarks
                $fastest_subject['time'] = min($fastest_subject['time'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['time']);
                $fastest_subject['memory'] = min($fastest_subject['memory'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['memory']);
                $slowest_subject['time'] = max($slowest_subject['time'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['time']);
                $slowest_subject['memory'] = max($slowest_subject['memory'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['memory']);
            }
        }

        // Percentage calculations for methods
        foreach ($codebench['benchmarks'] as & $method) {
            // Calculate percentage difference relative to fastest and slowest methods
            $method['percent']['fastest']['time'] = (empty($fastest_method['time'])) ? 0 : ($method['time'] / $fastest_method['time'] * 100);
            $method['percent']['fastest']['memory'] = (empty($fastest_method['memory'])) ? 0 : ($method['memory'] / $fastest_method['memory'] * 100);
            $method['percent']['slowest']['time'] = (empty($slowest_method['time'])) ? 0 : ($method['time'] / $slowest_method['time'] * 100);
            $method['percent']['slowest']['memory'] = (empty($slowest_method['memory'])) ? 0 : ($method['memory'] / $slowest_method['memory'] * 100);

            // Assign a grade for time and memory to each method
            $method['grade']['time'] = $this->_grade($method['percent']['fastest']['time']);
            $method['grade']['memory'] = $this->_grade($method['percent']['fastest']['memory']);

            // Percentage calculations for subjects
            foreach ($method['subjects'] as & $subject) {
                // Calculate percentage difference relative to fastest and slowest subjects for this method
                $subject['percent']['fastest']['time'] = (empty($fastest_subject['time'])) ? 0 : ($subject['time'] / $fastest_subject['time'] * 100);
                $subject['percent']['fastest']['memory'] = (empty($fastest_subject['memory'])) ? 0 : ($subject['memory'] / $fastest_subject['memory'] * 100);
                $subject['percent']['slowest']['time'] = (empty($slowest_subject['time'])) ? 0 : ($subject['time'] / $slowest_subject['time'] * 100);
                $subject['percent']['slowest']['memory'] = (empty($slowest_subject['memory'])) ? 0 : ($subject['memory'] / $slowest_subject['memory'] * 100);

                // Assign a grade letter for time and memory to each subject
                $subject['grade']['time'] = $this->_grade($subject['percent']['fastest']['time']);
                $subject['grade']['memory'] = $this->_grade($subject['percent']['fastest']['memory']);
            }
        }

        return $codebench;
    }

    /**
     * Callback for array_filter().
     * Filters out all methods not to benchmark.
     *
     * @param   string   method name
     * @return  boolean
     */
    protected function _method_filter($method)
    {
        // Only benchmark methods with the "bench" prefix
        return (substr($method, 0, 5) === 'bench');
    }

    /**
     * Returns the applicable grade letter for a score.
     *
     * @param   integer|double  score
     * @return  string  grade letter
     */
    protected function _grade($score)
    {
        foreach ($this->grades as $max => $grade) {
            if ($max === 'default')
                continue;

            if ($score <= $max)
                return $grade;
        }

        return $this->grades['default'];
    }

}
