MW

profile.class.php

Every now and then I want to profile a given part of PHP code. For example I want to quickly check wether my current changeset to GeSHi works faster or is horribly slower. For a big change I’ll stick to Xdebug and KCachegrind. But for a quick overview? Overkill in my eyes.

Say hello to profile.class.php, a simple timer class for PHP5 which you can use to get an overview about where how much time is spent. This is in no way a scientific method nor should you take the results of a single run as a basis for you decisions.

I’ve set an emphasize on an easy API so you don’t have to pollute your code with arbitrary hoops and whistles.

UPDATE: You can find the current updated source in the SVN repo of GeSHi.

Simple example

This is a quick example on how you could use the class:

    <?php
    require 'profile.class.php'; // should be obvious ;-)
     
    // here might be uninteresting code
     
    profile::start('overall'); // start a timer and give it a name
                                       // we add this one to get a time
                                       // for the overall runtime
     
    // this is the code you want to profile
    profile::start('$_SERVER');
    $foo = count($_SERVER);
    profile::stop(); // stop the last active counter
     
    profile::start('$GLOBALS');
    $bar = count($GLOBALS);
    profile::stop();
     
     
    profile::stop(); // stop overall timer
    profile::print_results(profile::flush()); // print the results

The output will be something like this:

    profile results
    --------------------------------------
    $GLOBALS      0.000027s        100.00%
    $_SERVER      0.000035s        128.07%
    overall       0.000212s        779.82%

Quite simple with a simple overview as a result.

Codeblocks

If you want to let some implementations of the same thing compete against each other do decide which one is the fastest, I’ve also added the codeblocks method. In the example below you can see one of my testcase which checks wether stristr or preg_match is faster to check wether a string is inside the other on a case insensitive base:

    <?php
    require 'profile.class.php';
     
    $string = file_get_contents('dummy_text.txt');
    $iterations = strlen($string);
     
    profile::codeblocks(array(
      'stristr' => 'if (stristr($string, "loRem")) {}',
      'preg_match' => 'if (preg_match("/loRem/i", $string)) {}',
    ), array(
      'string' => $string,
    ), $iterations);
     
    profile::print_results(profile::flush());

The results for a dummy text of about 24k size:

    profile results
    ----------------------------------------
    preg_match      0.229575s        100.00%
    stristr         2.601574s       1133.21%

So I’d say stristr is pretty slow!

The class

You can also download the file below.

    <?php
    /**
     * profile.class.php - A timer class for qualitative profiling
     *
     * This is a static class which you can use to qualitatively compare and
     * profile parts of your PHP code. It is as simple as
     *
     * <?php ...
     *   require 'profile.class.php';
     *   ...
     *   profile::start('some name');
     *   ...
     *   profile::stop();
     *   ...
     *   profile::print_results(profile::flush());
     * ?>
     *
     * The class itself should be self explaining and well documented. Take a look below!
     *
     *
     *
     * profile.class.php 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 2 of the License, or
     * (at your option) any later version.
     *
     * profile.class.php 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 profile.class.php; if not, write to the Free Software
     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     *
     * @author     Milian Wolff <mail@milianw.de>
     * @copyright  (C) 2008 Milian Wolff
     * @license    http://gnu.org/copyleft/gpl.html GNU GPL
     */
     
    class profile {
      // list of start and end timestamps
      static private $start = array();
      static private $end = array();
      // current names
      static private $running_profiles = array();
      static private $cur_name;
      /**
       * start the profile timer
       *
       * @param $name optional name for this profile
       */
      static public function start($name = 0) {
        if (!is_null(self::$cur_name)) {
          // nested timer
          // add old name to running profiles
          array_push(self::$running_profiles, self::$cur_name);
          if (self::$cur_name === $name) {
            if (is_int($name)) {
              $name++;
            } else {
              $name .= '-SUB';
            }
          }
        }
        if (!isset(self::$start[$name])) {
          // init
          self::$start[$name] = array();
          self::$end[$name] = array();
        }
        self::$cur_name = $name;
        // start timer
        array_push(self::$start[$name], microtime(true));
      }
      /**
       * stop the profile timer
       */
      static public function stop() {
        // stop timer
        array_push(self::$end[self::$cur_name], microtime(true));
        if (!empty(self::$running_profiles)) {
          // got a parent timer (nested timer)
          self::$cur_name = array_pop(self::$running_profiles);
        } else {
          self::$cur_name = null;
        }
      }
      /**
       * get the results and reset internal result cache
       *
       * @param $reverse boolean; calculate deviation to longest diff, defaults to false (shortest diff)
       * @return array of results
       */
      static public function flush($reverse = false) {
        if (!is_null(self::$cur_name)) {
          trigger_error('A timer is still running. Stop it before flushing the results by calling profile::stop().',
                              E_USER_ERROR);
          return;
        }
        if (empty(self::$start)) {
          return array();
        }
     
        $results = array();
        // reset vars
        $start = self::$start;
        $end = self::$end;
        self::$start = array();
        self::$end = array();
        self::$cur_name = null;
     
        $results = array();
        $diffs = array();
     
        // get runtimes
        $names = array_keys($start);
        $deviate_key = $names[0];
     
        foreach ($names as $key) {
          $diffs[$key] = array_sum($end[$key]) - array_sum($start[$key]);
          if (($reverse && $diffs[$key] > $diffs[$deviate_key])
              || (!$reverse && $diffs[$key] < $diffs[$deviate_key])) {
            // remember which run we take as reference point to calculate deviations
            $deviate_key = $key;
          }
        }
     
        if ($reverse) {
          arsort($diffs);
        } else {
          asort($diffs);
        }
     
        // calculate percental deviations and build up return array
        foreach ($diffs as $name => $diff) {
          array_push($results, array(
            'diff' => $diff,
            'start' => $start[$name],
            'end' => $end[$name],
            'name' => $name,
            'deviation' => ($diffs[$name] / $diffs[$deviate_key])*100,
          ));
        }
     
        return $results;
      }
      /**
       * default implementation as to how one could present the results, optimized for CLI usage
       *
       * @param $results an array as returned by profile::flush()
       * @param $dont_print optionally; only return the result and dont print it
       * @return string
       */
      static public function print_results($results, $dont_print = false) {
        $output = "profile results\n";
        if (empty($results)) {
          $output = "no code was profiled, empty resultset\n";
        } else {
          // get maximum col-width:
          $max_col_width = 0;
          for ($key = 0, $n = count($results); $key < $n; ++$key) {
            $max_col_width = max($max_col_width, strlen($results[$key]['name']));
          }
          $output .= str_repeat('-', $max_col_width + strlen('      0.000000s        100.00%')) ."\n";
     
          foreach ($results as $profile) {
            $output .= sprintf("%-". $max_col_width ."s    %10Fs    %10.2F%%\n",
                                       $profile['name'], $profile['diff'], $profile['deviation']);
          }
        }
        if (!$dont_print) {
          echo $output;
        }
        return $output;
      }
      /**
       * define code blocks and run them in random order, profiling each
       * this is great when writing little scripts to see which implementation of a given feature is faster
       *
       * @param $code_blocks an array with strings of php-code (just like eval or create_function accepts).
       *                     don't forget keys to give those codeblocks a name
       * @param $vars assoc array with global values which are used in the code blocks (i.e. varname => value)
       * @param $iterations number of times each block gets run
       * @return void
       */
      static public function codeblocks($code_blocks, $vars = array(), $iterations = 200) {
        if (!empty($vars)) {
          $vars_keys = '$'. implode(', $', array_keys($vars));
          $vars_values = array_values($vars);
        } else {
          $vars_keys = '';
          $vars_values = array();
        }
        $blocks = array();
        // pita to get random order
        foreach ($code_blocks as $name => $block) {
          $block = trim($block);
          if ($block[strlen($block) - 1] != ';') {
            $block .= ';';
          }
          array_push($blocks, array('name' => $name, 'code' => $block));
        }
        unset($code_blocks);
        shuffle($blocks);
        foreach ($blocks as $block) {
          $func = create_function($vars_keys, $block['code']);
          self::start($block['name']);
          for ($i = 0; $i < $iterations; ++$i) {
            call_user_func_array($func, $vars_values);
          }
          self::stop();
        }
      }
    }
Attachment Size
profile.class_.php.txt6.55 KB

Comments

Want to comment? Send me an email!

Published on June 24, 2008.