diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/classes/arrcallback.php b/classes/arrcallback.php new file mode 100644 index 0000000..5d9dbb3 --- /dev/null +++ b/classes/arrcallback.php @@ -0,0 +1,55 @@ + + */ +class Arrcallback extends Codebench { + + public $description = + 'Parsing "command[param,param]" strings in arr::callback(): + http://github.com/shadowhand/kohana/commit/c3aaae849164bf92a486e29e736a265b350cb4da#L0R127'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid callback strings + 'foo', + 'foo::bar', + 'foo[apple,orange]', + 'foo::bar[apple,orange]', + '[apple,orange]', // no command, only params + 'foo[[apple],[orange]]', // params with brackets inside + + // Invalid callback strings + 'foo[apple,orange', // no closing bracket + ); + + public function bench_shadowhand($subject) + { + // The original regex we're trying to optimize + if (preg_match('/([^\[]*+)\[(.*)\]/', $subject, $match)) + return $match; + } + + public function bench_geert_regex_1($subject) + { + // Added ^ and $ around the whole pattern + if (preg_match('/^([^\[]*+)\[(.*)\]$/', $subject, $matches)) + return $matches; + } + + public function bench_geert_regex_2($subject) + { + // A rather experimental approach using \K which requires PCRE 7.2 ~ PHP 5.2.4 + // Note: $matches[0] = params, $matches[1] = command + if (preg_match('/^([^\[]*+)\[\K.*(?=\]$)/', $subject, $matches)) + return $matches; + } + + public function bench_geert_str($subject) + { + // A native string function approach which beats all the regexes + if (strpos($subject, '[') !== FALSE AND substr($subject, -1) === ']') + return explode('[', substr($subject, 0, -1), 2); + } +} diff --git a/classes/codebench.php b/classes/codebench.php new file mode 100644 index 0000000..0f4c55c --- /dev/null +++ b/classes/codebench.php @@ -0,0 +1,3 @@ +request->redirect('codebench/'.strtolower(trim($_POST['class']))); + + // Pass the class name on to the view + $this->template->class = $class = ucfirst(trim($class)); + + // Try to load the class, then run it + if (Kohana::auto_load($class) === TRUE) + { + $codebench = new $class; + $this->template->codebench = $codebench->run(); + } + } +} diff --git a/classes/kohana/codebench.php b/classes/kohana/codebench.php new file mode 100644 index 0000000..6c9b5f5 --- /dev/null +++ b/classes/kohana/codebench.php @@ -0,0 +1,221 @@ + '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('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), array($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 = array('NULL' => NULL); + } + + // Initialize benchmark output + $codebench = array + ( + 'class' => get_class($this), + 'description' => $this->description, + 'loops' => array + ( + 'base' => (int) $this->loops, + 'total' => (int) $this->loops * count($this->subjects) * count($methods), + ), + 'subjects' => $this->subjects, + 'benchmarks' => array(), + ); + + // Benchmark each method + foreach ($methods as $method) + { + // Initialize benchmark output for this method + $codebench['benchmarks'][$method] = array('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 + Profiler::stop($token); + $benchmark = Profiler::total($token); + + // Temporary ugly workaround + $benchmark['time'] = $benchmark[0]; + $benchmark['memory'] = $benchmark[1]; + + // Benchmark output specific to the current method and subject + $codebench['benchmarks'][$method]['subjects'][$subject_key] = array + ( + 'return' => $return, + 'time' => $benchmark['time'], + 'memory' => $benchmark['memory'], + ); + + // Update method totals + $codebench['benchmarks'][$method]['time'] += $benchmark['time']; + $codebench['benchmarks'][$method]['memory'] += $benchmark['memory']; + } + } + + // 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 = array('time' => 999999999, 'memory' => 999999999); + $slowest_method = $slowest_subject = array('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'] = $method['time'] / $fastest_method['time'] * 100; + $method['percent']['fastest']['memory'] = $method['memory'] / $fastest_method['memory'] * 100; + $method['percent']['slowest']['time'] = $method['time'] / $slowest_method['time'] * 100; + $method['percent']['slowest']['memory'] = $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'] = $subject['time'] / $fastest_subject['time'] * 100; + $subject['percent']['fastest']['memory'] = $subject['memory'] / $fastest_subject['memory'] * 100; + $subject['percent']['slowest']['time'] = $subject['time'] / $slowest_subject['time'] * 100; + $subject['percent']['slowest']['memory'] = $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']; + } +} diff --git a/classes/ltrimdigits.php b/classes/ltrimdigits.php new file mode 100644 index 0000000..b080c37 --- /dev/null +++ b/classes/ltrimdigits.php @@ -0,0 +1,26 @@ + + */ +class Ltrimdigits extends Codebench { + + public $description = 'Chopping off leading digits: regex vs ltrim.'; + + public $loops = 100000; + + public $subjects = array + ( + '123digits', + 'no-digits', + ); + + public function bench_regex($subject) + { + return preg_replace('/^\d+/', '', $subject); + } + + public function bench_ltrim($subject) + { + return ltrim($subject, '0..9'); + } +} diff --git a/classes/validcolor.php b/classes/validcolor.php new file mode 100644 index 0000000..494190f --- /dev/null +++ b/classes/validcolor.php @@ -0,0 +1,114 @@ + + */ +class ValidColor extends Codebench { + + public $description = + 'Optimization for Validate::color(). + See: http://forum.kohanaphp.com/comments.php?DiscussionID=2192. + + Note that the methods with an _invalid suffix contain flawed regexes and should be + completely discarded. I left them in here for educational purposes, and to remind myself + to think harder and test more thoroughly. It can\'t be that I only found out so late in + the game. For the regex explanation have a look at the forum topic mentioned earlier.'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid colors + 'aaA', + '123', + '000000', + '#123456', + '#abcdef', + + // Invalid colors + 'ggg', + '1234', + '#1234567', + "#000\n", + '}§è!çà%$z', + ); + + // Note that I added the D modifier to corey's regexes. We need to match exactly + // the same if we want the benchmarks to be of any value. + public function bench_corey_regex_1_invalid($subject) + { + return (bool) preg_match('/^#?([0-9a-f]{1,2}){3}$/iD', $subject); + } + + public function bench_corey_regex_2($subject) + { + return (bool) preg_match('/^#?([0-9a-f]){3}(([0-9a-f]){3})?$/iD', $subject); + } + + // Optimized corey_regex_1 + // Using non-capturing parentheses and a possessive interval + public function bench_geert_regex_1a_invalid($subject) + { + return (bool) preg_match('/^#?(?:[0-9a-f]{1,2}+){3}$/iD', $subject); + } + + // Optimized corey_regex_2 + // Removed useless parentheses, made the remaining ones non-capturing + public function bench_geert_regex_2a($subject) + { + return (bool) preg_match('/^#?[0-9a-f]{3}(?:[0-9a-f]{3})?$/iD', $subject); + } + + // Optimized geert_regex_1a + // Possessive "#" + public function bench_geert_regex_1b_invalid($subject) + { + return (bool) preg_match('/^#?+(?:[0-9a-f]{1,2}+){3}$/iD', $subject); + } + + // Optimized geert_regex_2a + // Possessive "#" + public function bench_geert_regex_2b($subject) + { + return (bool) preg_match('/^#?+[0-9a-f]{3}(?:[0-9a-f]{3})?$/iD', $subject); + } + + // Using \z instead of $ + public function bench_salathe_regex_1($subject) + { + return (bool) preg_match('/^#?+[0-9a-f]{3}(?:[0-9a-f]{3})?\z/i', $subject); + } + + // Using \A instead of ^ + public function bench_salathe_regex_2($subject) + { + return (bool) preg_match('/\A#?+[0-9a-f]{3}(?:[0-9a-f]{3})?\z/i', $subject); + } + + // A solution without regex + public function bench_geert_str($subject) + { + if ($subject[0] === '#') + { + $subject = substr($subject, 1); + } + + $strlen = strlen($subject); + return (($strlen === 3 OR $strlen === 6) AND ctype_xdigit($subject)); + } + + // An ugly, but fast, solution without regex + public function bench_salathe_str($subject) + { + if ($subject[0] === '#') + { + $subject = substr($subject, 1); + } + + // TRUE if: + // 1. $subject is 6 or 3 chars long + // 2. $subject contains only hexadecimal digits + return (((isset($subject[5]) AND ! isset($subject[6])) OR + (isset($subject[2]) AND ! isset($subject[3]))) + AND ctype_xdigit($subject)); + } +} diff --git a/config/codebench.php b/config/codebench.php new file mode 100644 index 0000000..2776b46 --- /dev/null +++ b/config/codebench.php @@ -0,0 +1,16 @@ + 0, + + /** + * Expand all benchmark details by default. + */ + 'expand_all' => FALSE, + +); diff --git a/init.php b/init.php new file mode 100644 index 0000000..866cc34 --- /dev/null +++ b/init.php @@ -0,0 +1,8 @@ +)') + ->defaults(array( + 'controller' => 'codebench', + 'action' => 'index', + 'class' => NULL)); diff --git a/views/codebench.php b/views/codebench.php new file mode 100644 index 0000000..56170d7 --- /dev/null +++ b/views/codebench.php @@ -0,0 +1,256 @@ + + + + + + + + <?php echo $class ?> Codebench + + + + + + + + + + + +
+

+ + + + + Library not found + + No methods found to benchmark + + +

+
+ + + + + +

+ + Remember to prefix the methods you want to benchmark with “bench”.
+ You might also want to overwrite Codebench->method_filter(). +
+

+ + + + + + + + + + + + Raw output:', Kohana::debug($codebench) ?> + + + + + + + \ No newline at end of file