Calculate script execution time (PHP class)

Sometimes when writing PHP code that is supposed to run fast we use the script execution calculation functions. Usually it involves (at least for me :) copying the code from example #1 on PHP's microtime function page and pasting it into your script only to do the same all over again the next time.

However, as good as that script is, it is not versatile enough. It does not, for example, allow you to time execution of only some lines or even more - some separated lines.

Let's look at an example:

  1. for ($i = 0; $i < 100; $i++) {
  2.      fx();
  3.      fy();
  4.      fz();
  5. }

What if we needed to time the execution of fy()? It's not impossible to do using the usual measures however it becomes a bit tedious. We would have to have multiple microtime calls, some local variable to store the total time and so on.

Therefore, to make my and hopefully someone else's life easier, I wrote a class for those occasions. It allows you to start, stop, pause and resume timing of specific parts of your code. It would even allow you to calculate total time it takes to run fx() AND fz() together.

Here is the full source code of the class:

  1. <?php
  2.  
  3. class Timer
  4. {
  5.  
  6.     // command constants
  7.     const CMD_START = 'start';
  8.     const CMD_STOP = 'end';
  9.  
  10.     // return format constants
  11.     const SECONDS = 0;
  12.     const MILLISECONDS = 1;
  13.     const MICROSECONDS = 2;
  14.  
  15.     // number of microseconds in a second
  16.     const USECDIV = 1000000;
  17.  
  18.  
  19.     /**
  20.      * Stores current state of the timer
  21.      *
  22.      * @var boolean
  23.      */
  24.     private static $_running = false;
  25.  
  26.     /**
  27.      * Contains the queue of times
  28.      *
  29.      * @var array
  30.      */
  31.     private static $_queue = array();
  32.  
  33.  
  34.     /**
  35.      * Start the timer
  36.      *
  37.      * @return void
  38.      */
  39.     public static function start()
  40.     {
  41.         // push current time
  42.         self::_pushTime(self::CMD_START);
  43.     }
  44.  
  45.  
  46.     /**
  47.      * Stop the timer
  48.      *
  49.      * @return void
  50.      */
  51.     public static function stop()
  52.     {
  53.         // push current time
  54.         self::_pushTime(self::CMD_STOP);
  55.     }
  56.  
  57.  
  58.     /**
  59.      * Reset contents of the queue
  60.      *
  61.      * @return void
  62.      */
  63.     public static function reset()
  64.     {
  65.         // reset the queue
  66.         self::$_queue = array();
  67.     }
  68.  
  69.  
  70.     /**
  71.      * Add a time entry to the queue
  72.      *
  73.      * @param string $cmd Command to push
  74.      * @return void
  75.      */
  76.     private static function _pushTime($cmd)
  77.     {
  78.         // capture the time as early in the function as possible
  79.         $mt = microtime();
  80.  
  81.         // set current running state depending on the command
  82.         if ($cmd == self::CMD_START) {
  83.             // check if the timer has already been started
  84.             if (self::$_running === true) {
  85.                 trigger_error('Timer has already been started', E_USER_NOTICE);
  86.                 return;
  87.             }
  88.  
  89.             // set current state
  90.             self::$_running = true;
  91.  
  92.         } else if ($cmd == self::CMD_STOP) {
  93.             // check if the timer is already stopped
  94.             if (self::$_running === false) {
  95.                 trigger_error('Timer has already been stopped/paused or has not yet been started', E_USER_NOTICE);
  96.                 return;
  97.             }
  98.  
  99.             // set current state
  100.             self::$_running = false;
  101.  
  102.         } else {
  103.             // fail execution of the script
  104.             trigger_error('Invalid command specified', E_USER_ERROR);
  105.             return;
  106.         }
  107.        
  108.         // recapture the time as close to the end of the function as possible
  109.         if ($cmd === self::CMD_START) {
  110.             $mt = microtime();
  111.         }
  112.        
  113.         // split the time into components
  114.         list($usec, $sec) = explode(' ', $mt);
  115.  
  116.         // typecast them to the required types
  117.         $sec = (int) $sec;
  118.         $usec = (float) $usec;
  119.         $usec = (int) ($usec * self::USECDIV);
  120.  
  121.         // create the array
  122.         $time = array(
  123.             $cmd => array(
  124.                 'sec'   => $sec,
  125.                 'usec'  => $usec,
  126.             ),
  127.         );
  128.  
  129.         // add a time entry depending on the command
  130.         if ($cmd == self::CMD_START) {
  131.             array_push(self::$_queue, $time);
  132.  
  133.         } else if ($cmd == self::CMD_STOP) {
  134.             $count = count(self::$_queue);
  135.             $array =& self::$_queue[$count - 1];
  136.             $array = array_merge($array, $time);
  137.         }
  138.     }
  139.  
  140.  
  141.     /**
  142.      * Get time of execution from all queue entries
  143.      *
  144.      * @param int $format Format of the returned data
  145.      * @return int|float
  146.      */
  147.     public static function get($format = self::SECONDS)
  148.     {
  149.         // stop timer if it is still running
  150.         if (self::$_running === true) {
  151.             trigger_error('Forcing timer to stop', E_USER_NOTICE);
  152.             self::stop();
  153.         }
  154.  
  155.         // reset all values
  156.         $sec = 0;
  157.         $usec = 0;
  158.  
  159.         // loop through each time entry
  160.         foreach (self::$_queue as $time) {
  161.             // start and end times
  162.             $start = $time[self::CMD_START];
  163.             $end = $time[self::CMD_STOP];
  164.  
  165.             // calculate difference between start and end seconds
  166.             $sec_diff = $end['sec'] - $start['sec'];
  167.  
  168.             // if starting and finishing seconds are the same
  169.             if ($sec_diff === 0) {
  170.                 // only add the microseconds difference
  171.                 $usec += ($end['usec'] - $start['usec']);
  172.  
  173.             } else {
  174.                 // add the difference in seconds (compensate for microseconds)
  175.                 $sec += $sec_diff - 1;
  176.  
  177.                 // add the difference time between start and end microseconds
  178.                 $usec += (self::USECDIV - $start['usec']) + $end['usec'];
  179.             }
  180.         }
  181.  
  182.         if ($usec > self::USECDIV) {
  183.             // move the full second microseconds to the seconds' part
  184.             $sec += (int) floor($usec / self::USECDIV);
  185.  
  186.             // keep only the microseconds that are over the self::USECDIV
  187.             $usec = $usec % self::USECDIV;
  188.         }
  189.  
  190.         switch ($format) {
  191.             case self::MICROSECONDS:
  192.                 return ($sec * self::USECDIV) + $usec;
  193.  
  194.             case self::MILLISECONDS:
  195.                 return ($sec * 1000) + (int) round($usec / 1000, 0);
  196.  
  197.             case self::SECONDS:
  198.             default:
  199.                 return (float) $sec + (float) ($usec / self::USECDIV);
  200.         }
  201.     }
  202.    
  203.    
  204.     /**
  205.      * Get the average time of execution from all queue entries
  206.      *
  207.      * @param int $format Format of the returned data
  208.      * @return float
  209.      */
  210.     public static function getAverage($format = self::SECONDS)
  211.     {
  212.         $count = count(self::$_queue);
  213.         $sec = 0;
  214.         $usec = self::get(self::MICROSECONDS);
  215.  
  216.         if ($usec > self::USECDIV) {
  217.             // move the full second microseconds to the seconds' part
  218.             $sec += (int) floor($usec / self::USECDIV);
  219.  
  220.             // keep only the microseconds that are over the self::USECDIV
  221.             $usec = $usec % self::USECDIV;
  222.         }
  223.  
  224.         switch ($format) {
  225.             case self::MICROSECONDS:
  226.                 $value = ($sec * self::USECDIV) + $usec;
  227.                 return round($value / $count, 2);
  228.  
  229.             case self::MILLISECONDS:
  230.                 $value = ($sec * 1000) + (int) round($usec / 1000, 0);
  231.                 return round($value / $count, 2);
  232.  
  233.             case self::SECONDS:
  234.             default:
  235.                 $value = (float) $sec + (float) ($usec / self::USECDIV);
  236.                 return round($value / $count, 2);
  237.         }
  238.     }
  239.  
  240. }

First, let's look at the public methods that are available to you:

  • start() - start/resume the timer
  • stop() - stop/pause the timer
  • reset() - reset the timer
  • get([$format]) - stops the timer (if required) and returns the amount of time as number of seconds, milliseconds or microseconds. The type of the result depends on the value of the $format parameter. The format can be one of the following three: Timer::SECONDS (default), Timer::MILLISECONDS or Timer::MICROSECONDS. if the format is defined as seconds, the result will be a float, otherwise an int.

How it works

The class has a "queue" of times. One Timer::start()/Timer::stop() cycle counts as one entry in the queue with its own execution time. Therefore if you only do start() and stop() once, one queue item will be added. If you call Timer::start() and Timer::stop() multiple times without calling Timer::reset(), multiple entries will get added to the queue.

Then, when you call Timer::get(), the function will go through every queue entry, calculate the time that it took to execute and sum everything up to get the total time.

Whenever you need to restart the calculation just call Timer::reset(). That will clear the queue of current times.

Examples

The first and the most usual scenario, where you want to calculate the time it takes to run your whole script, is the easiest one. Just add Timer::start() at beginning of the file/script and print Timer::get(); at the end.

Next, let's rewrite the examples we mentioned earlier. As you will see it is not much more different than timing the whole script:

  1. for ($i = 0; $i < 100; $i++) {
  2.      fx();
  3.      // calculate the time it takes to run fy()
  4.      Timer::start();
  5.      fy();
  6.      Timer::stop();
  7.      fz();
  8. }
  9.  
  10. print Timer::get();
  11. // or
  12. print Timer::get(Timer::MICROSECONDS);
  13. // or
  14. print Timer::get(Timer::MILLISECONDS);

As previously mentioned, if you wanted to calculate the total time it takes to execute fx() AND fz() we would need to adjust the code to wrap two blocks of code instead of just one:

  1. for ($i = 0; $i < 100; $i++) {
  2.      // calculate the time it takes to run fx()
  3.      Timer::start();
  4.      fx();
  5.      Timer::stop();
  6.  
  7.      fy();
  8.  
  9.      // calculate the time it takes to run fz()
  10.      Timer::start();
  11.      fz();
  12.      Timer::stop();
  13. }
  14.  
  15. print Timer::get();

In case you want to track how much time it has taken for the script to run "up to now" you can simply call the Timer::get() function in the middle of the loop:

  1. for ($i = 0; $i < 100; $i++) {
  2.      fx();
  3.      // calculate the time it takes to run fy()
  4.      Timer::start();
  5.      fy();
  6.      Timer::stop();
  7.      fz();
  8.  
  9.      print Timer::get();
  10. }

IMPORTANT!
Timer::get() also stops the timer as it needs to set the final queue entry's end time. Therefore, if you call it between two blocks of code where the time gets calculated, you need to make sure you resume the timer by calling Timer::start().

Non-static version of the class

For those that don't like static classes or who might need multiple instances of the Timer class, here is a modified version of the static class:

  1. <?php
  2.  
  3. class Timer
  4. {
  5.  
  6.     // command constants
  7.     const CMD_START = 'start';
  8.     const CMD_STOP = 'end';
  9.  
  10.     // return format constants
  11.     const SECONDS = 0;
  12.     const MILLISECONDS = 1;
  13.     const MICROSECONDS = 2;
  14.  
  15.     // number of microseconds in a second
  16.     const USECDIV = 1000000;
  17.  
  18.  
  19.     /**
  20.      * Stores current state of the timer
  21.      *
  22.      * @var boolean
  23.      */
  24.     private $_running = false;
  25.  
  26.     /**
  27.      * Contains the queue of times
  28.      *
  29.      * @var array
  30.      */
  31.     private $_queue = array();
  32.  
  33.  
  34.     /**
  35.      * Start the timer
  36.      *
  37.      * @return void
  38.      */
  39.     public function start()
  40.     {
  41.         // push current time
  42.         $this->_pushTime(self::CMD_START);
  43.     }
  44.  
  45.  
  46.     /**
  47.      * Stop the timer
  48.      *
  49.      * @return void
  50.      */
  51.     public function stop()
  52.     {
  53.         // push current time
  54.         $this->_pushTime(self::CMD_STOP);
  55.     }
  56.  
  57.  
  58.     /**
  59.      * Reset contents of the queue
  60.      *
  61.      * @return void
  62.      */
  63.     public function reset()
  64.     {
  65.         // reset the queue
  66.         $this->_queue = array();
  67.     }
  68.  
  69.  
  70.     /**
  71.      * Add a time entry to the queue
  72.      *
  73.      * @param string $cmd Command to push
  74.      * @return void
  75.      */
  76.     private function _pushTime($cmd)
  77.     {
  78.         // capture the time as early in the function as possible
  79.         $mt = microtime();
  80.  
  81.         // set current running state depending on the command
  82.         if ($cmd == self::CMD_START) {
  83.             // check if the timer has already been started
  84.             if ($this->_running === true) {
  85.                 trigger_error('Timer has already been started', E_USER_NOTICE);
  86.                 return;
  87.             }
  88.  
  89.             // set current state
  90.             $this->_running = true;
  91.  
  92.         } else if ($cmd == self::CMD_STOP) {
  93.             // check if the timer is already stopped
  94.             if ($this->_running === false) {
  95.                 trigger_error('Timer has already been stopped/paused or has not yet been started', E_USER_NOTICE);
  96.                 return;
  97.             }
  98.  
  99.             // set current state
  100.             $this->_running = false;
  101.  
  102.         } else {
  103.             // fail execution of the script
  104.             trigger_error('Invalid command specified', E_USER_ERROR);
  105.             return;
  106.         }
  107.        
  108.         // recapture the time as close to the end of the function as possible
  109.         if ($cmd === self::CMD_START) {
  110.             $mt = microtime();
  111.         }
  112.        
  113.         // split the time into components
  114.         list($usec, $sec) = explode(' ', $mt);
  115.  
  116.         // typecast them to the required types
  117.         $sec = (int) $sec;
  118.         $usec = (float) $usec;
  119.         $usec = (int) ($usec * self::USECDIV);
  120.  
  121.         // create the array
  122.         $time = array(
  123.             $cmd => array(
  124.                 'sec'   => $sec,
  125.                 'usec'  => $usec,
  126.             ),
  127.         );
  128.  
  129.         // add a time entry depending on the command
  130.         if ($cmd == self::CMD_START) {
  131.             array_push($this->_queue, $time);
  132.  
  133.         } else if ($cmd == self::CMD_STOP) {
  134.             $count = count($this->_queue);
  135.             $array =& $this->_queue[$count - 1];
  136.             $array = array_merge($array, $time);
  137.         }
  138.     }
  139.  
  140.  
  141.     /**
  142.      * Get time of execution from all queue entries
  143.      *
  144.      * @param int $format Format of the returned data
  145.      * @return int|float
  146.      */
  147.     public function get($format = self::SECONDS)
  148.     {
  149.         // stop timer if it is still running
  150.         if ($this->_running === true) {
  151.             trigger_error('Forcing timer to stop', E_USER_NOTICE);
  152.             $this->stop();
  153.         }
  154.  
  155.         // reset all values
  156.         $sec = 0;
  157.         $usec = 0;
  158.  
  159.         // loop through each time entry
  160.         foreach ($this->_queue as $time) {
  161.             // start and end times
  162.             $start = $time[self::CMD_START];
  163.             $end = $time[self::CMD_STOP];
  164.  
  165.             // calculate difference between start and end seconds
  166.             $sec_diff = $end['sec'] - $start['sec'];
  167.  
  168.             // if starting and finishing seconds are the same
  169.             if ($sec_diff === 0) {
  170.                 // only add the microseconds difference
  171.                 $usec += ($end['usec'] - $start['usec']);
  172.  
  173.             } else {
  174.                 // add the difference in seconds (compensate for microseconds)
  175.                 $sec += $sec_diff - 1;
  176.  
  177.                 // add the difference time between start and end microseconds
  178.                 $usec += (self::USECDIV - $start['usec']) + $end['usec'];
  179.             }
  180.         }
  181.  
  182.         if ($usec > self::USECDIV) {
  183.             // move the full second microseconds to the seconds' part
  184.             $sec += (int) floor($usec / self::USECDIV);
  185.  
  186.             // keep only the microseconds that are over the self::USECDIV
  187.             $usec = $usec % self::USECDIV;
  188.         }
  189.  
  190.         switch ($format) {
  191.             case self::MICROSECONDS:
  192.                 return ($sec * self::USECDIV) + $usec;
  193.  
  194.             case self::MILLISECONDS:
  195.                 return ($sec * 1000) + (int) round($usec / 1000, 0);
  196.  
  197.             case self::SECONDS:
  198.             default:
  199.                 return (float) $sec + (float) ($usec / self::USECDIV);
  200.         }
  201.     }
  202.    
  203.    
  204.     /**
  205.      * Get the average time of execution from all queue entries
  206.      *
  207.      * @param int $format Format of the returned data
  208.      * @return float
  209.      */
  210.     public static function getAverage($format = self::SECONDS)
  211.     {
  212.         $count = count($this->_queue);
  213.         $sec = 0;
  214.         $usec = $this->get(self::MICROSECONDS);
  215.  
  216.         if ($usec > self::USECDIV) {
  217.             // move the full second microseconds to the seconds' part
  218.             $sec += (int) floor($usec / self::USECDIV);
  219.  
  220.             // keep only the microseconds that are over the self::USECDIV
  221.             $usec = $usec % self::USECDIV;
  222.         }
  223.  
  224.         switch ($format) {
  225.             case self::MICROSECONDS:
  226.                 $value = ($sec * self::USECDIV) + $usec;
  227.                 return round($value / $count, 2);
  228.  
  229.             case self::MILLISECONDS:
  230.                 $value = ($sec * 1000) + (int) round($usec / 1000, 0);
  231.                 return round($value / $count, 2);
  232.  
  233.             case self::SECONDS:
  234.             default:
  235.                 $value = (float) $sec + (float) ($usec / self::USECDIV);
  236.                 return round($value / $count, 2);
  237.         }
  238.     }
  239.  
  240. }

The way you would use this class is pretty similar to the static class except you can instantiate it and have multiple timers running on your page:

  1. // instantiate two copies of the Timer class
  2. $t1 = new Timer();
  3. $t2 = new Timer();
  4.  
  5. for ($i = 0; $i < 100; $i++) {
  6.       // calculate the time it takes to run fx() using timer #1
  7.       $t1->start();
  8.       fx();
  9.       $t1->stop();
  10.  
  11.       // calculate the time it takes to run fy() using timer #2
  12.       $t2->start();
  13.       fy();
  14.       $t2->stop();
  15. }
  16.  
  17. // get results
  18. print $t1->get();
  19. print $t2->get(Timer::MILLISECONDS);

Hopefully these examples help. If not, please leave your comments with your questions or suggestions.

Comments
1
This is an excellent class, thank you very much for posting it :)
Mike, September 6th 2010, 12:43
2
My pleasure :)
Andris, September 9th 2010, 16:03
3
Thank you so much! this is wonderful script - like you said - you this script will save lots of time and will do what initial php function does not do. plus, your solution of this problem is the best, on my opinion with implementations of start, stop, get and reset.

thank you!
Slava, October 8th 2010, 20:31
4
I just require_once( timer.php ) and it crashes !!

Don't you have a download method ????

Steve
Steve Anderson, October 18th 2010, 16:15
5
Steve - yes, if you scroll down the code div, there's a "download this snippet" link. Alternatively you can just click here for the static version or here for the non-static one.

Regarding crashing - I suppose you are not seeing any error messages. In that case make sure you have 
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
added before your code.
Andris, October 18th 2010, 19:31
6
This is ROCK! Thank you for the beautiful time saver :)
however, how if you add a method to count the average time execution?
aprillins, October 27th 2010, 17:45
7
@aprillins:

Try adding this function to the non-static version:

/**
 * Get the average time of execution from all queue entries
 *
 * @return float
 */
public function getAverage($format = self::SECONDS)
{
	$count = count($this->_queue);
	$sec = 0;
	$usec = $this->get(self::MICROSECONDS);

	if ($usec > self::USECDIV) {
		// move the full second microseconds to the seconds' part
		$sec += (int) floor($usec / self::USECDIV);

		// keep only the microseconds that are over the self::USECDIV
		$usec = $usec % self::USECDIV;
	}

	switch ($format) {
		case self::MICROSECONDS:
			$value = ($sec * self::USECDIV) + $usec;
			return round($value / $count, 2);

		case self::MILLISECONDS:
			$value = ($sec * 1000) + (int) round($usec / 1000, 0);
			return round($value / $count, 2);

		case self::SECONDS:
		default:
			$value = (float) $sec + (float) ($usec / self::USECDIV);
			return round($value / $count, 2);
	}
}
Andris, November 16th 2010, 12:28
8
Nice one :) I was making my own system but I found this class :)

Thanks :)
Vince, December 28th 2010, 15:39
9
A good one, thanks, I m just making a MLM Tree Node system for a company, I needed to test the performance, thanks you so much!!!
Jason Hoi, February 18th 2011, 10:53
10
well done in these classes... 

I think i'd recommend doing one thing tho  as I used the static version and it takes longer on the first run... 

( in fact about 30 microseconds longer... obviously cuz it's setting things up for the first time - unless i'm doin something daft).

so, i used it this way -
1. start/stop/reset - one time thru first.... (throw away run...)
2. then use it as stated above, and the next results will be extremely accurate.

I ran the same script consecutively 4-5 times, and the 1st one always was 30 (+/- 1) microseconds more than the other 3-4 times... those 3-4 runs were within 1 microsecond of each other.

just a thought is all.
Bill Ortell, March 3rd 2011, 14:31
11
You are absolutely right, Bill. It is a bit of an issue. There will always be a bit of an overhead while using these classes comparing to using "raw" microtime().

The start() and stop() methods will add some small amount of time to the total every time they are called as 1) the code will need to dereference the pointer to call those functions and 2) within those functions the code will need to find the pointer to the _pushTime() method in the vtable.

I put the call to the microtime() function as the first line of the function thinking that I will get the "freshest" time like that. However, it's not really a solution as there is a bunch of code being executed afterwards anyway, which means that the execution of the user's code will get delayed till the end of the execution of _pushTime().

I am not sure if that makes sense to you :) But the short answer is - no class will ever be as precise as microtime() alone just because of the time that is required to execute the code inside the very class.

In fact I was thinking that the class could be optimized a bit more by placing the call to microtime() at the top of the function for stop() and as low as possible in the function for all start() calls.
Andris, March 4th 2011, 15:41
12
Update: moved the lines around a bit in the _pushTime() method. Did it so that if the type of the request is "start", it calls the microtime() as close to the end of the method as possible. This way the actual time should be "closer" to the user code than it was before when there was still the if/else statement in between.

Time for stop() is fetched in the beginning of the method, which again means that almost nothing happens between the user code and the time fetching. 

Should improve things a bit, would be interesting if someone could actually test the difference.
Andris, March 4th 2011, 15:54
13
Please fix this line


$count = count($self::$_queue);


to 


$count = count(self::$_queue);
SM@K, March 10th 2011, 8:54
14
@ SM@K: done. Thanks for spotting it!
Andris, March 10th 2011, 18:48
15
thanks alot for providing this class :) found it through the php.net manual @ microtime function description (although i was googling for "php execution time")
cookie, March 24th 2011, 22:16
16
Very nice script, however i cant seem to get Timer::get(Timer::MILLISECONDS) to work. Every time i try to print it all i get is 0.

Timer::get(Timer::MICROSECONDS) seems to return a value however.
Leo, July 29th 2011, 3:51
17
Hi Leo,

Can you post the part of your code where you are using the Timer class? I tried the following script and it worked fine for me:
Timer::start();
for ($i = 0; $i < 1000000; $i++) {
	$x = log($i);
}
Timer::stop();

var_dump(Timer::get(Timer::MILLISECONDS)); // int(728)
var_dump(Timer::get(Timer::MICROSECONDS)); // int(728340)
var_dump(Timer::get(Timer::SECONDS));      // float(0.72834)
Andris, July 30th 2011, 18:01
18
Thanks so much - excellent script!
Rob Anderson, March 30th 2012, 4:19
19
Thanks a lot for your kindness and support. Hopefully you will keep it continue....
Imteyaz, May 30th 2012, 12:33
20
Thank you for your kind words, Imteyaz! I'm glad I could help you!
Andris, June 5th 2012, 23:52
21
thanks a lot for your nice time-saving script :-D!!!!!
Aneta, August 5th 2013, 10:43
22
very practical, thank you
;-)
André, October 2nd 2013, 15:17
Name
Email (required)
will not be published
Website
Recaptcha
you will only be required to fill it in once in this session

You can use [code][/code] tags in your comments