Whether you’re using Adobe Scout or good old getTimer, there is a fundamental limitation: all times are in whole milliseconds. This is an issue if you’re trying to measure code that executes very quickly or compare code that has only minor differences. In these cases you get inconsistent results (7ms, 8ms, 7ms, 7ms, 8ms, …) when you’d much rather have better accuracy (7.3ms) with sub-millisecond precision. Today’s article introduces a new helper class called Timely that makes sub-millisecond precision a snap. Read on for the source code and an example app.

First things first, today’s article is heavily inspired by a technique described recently by Benjamin Guihaire. There are, however, some differences in our two approaches to sub-millisecond timing, but comparison is not the focus of this article. If you’re interested, check out his article on the topic and the rest of his site while you’re at it. It’s a great read.

As for Timely, it’s designed to not require anything other than plain old Flash Player 9 (AS3) and either ASC 1.0 or 2.0. Using it instead of getTimer is easy, too. Instead of:

var beginTime:int = getTimer();
// ... do work here ...
var endTime:int = getTimer();
var elapsed:int = endTime - beginTime;

You can use Timely like this:

var timer:Timely = new Timely();
timer.begin();
// ... do work here ...
var elapsed:Number = timer.end();

Feel free to re-use the Timely object as many times as you’d like. You’ll most likely only need to create one of them.

Timely relies on a calibration step that automatically takes place in its constructor. Since this step can take up to 10 milliseconds, you can delay it and manually calibrate later:

var timer:Timely = new Timely(false);
// ... do other work that doesn't involve timing ...
timer.calibrate();
// ... now use timer for timing ...

Doing this also lets you choose to spend less or more time calibrating the timer:

var timer:Timely = new Timely(false);
// ... do other work that doesn't involve timing ...
timer.calibrate(1000); // spend a whole second calibrating
// ... now use timer for timing ...

You can also check to see the results of calibration, which may be a useful statistic for performance comparison:

var timer:Timely = new Timely();
trace("This computer can perform " + timer.iterationsPerMS + " iterations per second");

And finally, you can get the beginning time of the timer for your own purposes:

var timer:Timely = new Timely();
var beginTime:int = timer.begin(); // get the current time after begin() returns
timer.end();

And that’s all for the Timely API. It’s designed to be simple to use, add minimal bloat to your SWF, have zero dependencies, and even be extendable in case you’d like to subclass it to add or change any functionality. Of course, as with all code I post on this site, it’s also open source under the extremely permissive MIT license.

As for how it works, there are three main concepts. The first is that when you call getTimer you have no idea where in the millisecond you are. You could be 0.1 milliseconds into it or 0.99. Since you only get a whole int number, you just don’t know. So the first step is to construct a very tight loop that simply checks getTimer over and over until its return value changes. At that point at least we know that we’re close to the beginning of the millisecond. We’re probably not exactly at the beginning, but perhaps a few nanoseconds after it. This is, therefore, an approximation system but one that is far more accurate than simply using whole milliseconds.

Second, in the calibration step we perform more of these tight loops checking how many of them we can do in one millisecond. For accuracy, it’s good to use many milliseconds and take an average rather than just one millisecond.

Third, when we stop the timer we again don’t know where we are in the millisecond. However, we can perform the same sort of loop again to check the time until it changes. We can compare the number of iterations of this loop to the number of iterations we could do in one millisecond that we obtained during calibration and use this to approximate the portion of the millisecond that was not used when the timer was stopped.

For ultimate detail, please read the following source code of Timely. It is almost entirely made up of comments and there is very little actual code within it. This is to make it as readable as possible for the purposes of this article, among other. To really understand how this works, have a read and keep in mind that “comments are free”!

package
{
	import flash.utils.getTimer;
 
	/**
	* A timer that gives sub-millisecond estimates that are more accurate than the
	* whole-millisecond values provided by raw usage of flash.utils.getTimer.
	* 
	* Inspired by Benjamin Guihaire's excellent article on the same topic that
	* provides a similar class: AccurateTimer (http://guihaire.com/code/?p=791)
	* 
	* @author Jackson Dunstan (http://JacksonDunstan.com)
	* @license MIT (http://opensource.org/licenses/MIT)
	*/
	public class Timely
	{
		/** The number of "do{someInt++;}while(getTimer()<intVal)" iterations
		*** that can be run in one millisecond at the time that calibrate() was
		*** called. This is initially set to zero and therefore the timer is not
		*** usable until this is set. Overwriting this value will change the
		*** results of this timer and is only recommended if you have a suitable
		*** value such as one read from another timer. Reading this value may be
		*** useful for comparing performance or other statistics in addition to
		*** aiding in debugging efforts. */
		public var iterationsPerMS:Number = 0;
 
		/** Time from getTimer() after the last call to begin() */
		public var beginTime:int;
 
		/**
		* Create the timer and optionally calibrate it
		* @param calibrate Calibrate the timer by calling calibrate(). Defaults
		*                  to true. If false, you'll need to call calibrate()
		*                  before using it or set iterationsPerMS manually.
		*/
		public function Timely(calibrate:Boolean=true)
		{
			if (calibrate)
			{
				this.calibrate();
			}
		}
 
		/**
		* Calibrate the timer. You must call this function or manually set
		* iterationsPerSecond before using the timer or your results will be
		* highly inaccurate.
		* @param msToCalibrate The number of milliseconds to spend calibrating
		*                      the timer. Higher values will increase the
		*                      timer's accuracy. This is clamped to at least one
		*                      millisecond and therefore calibration is quite
		*                      slow compared to most operations.
		* @return The number of iterations per millisecond
		*/
		public function calibrate(msToCalibrate:int=10): Number
		{
			// Must spend at least one millisecond calibrating
			if (msToCalibrate < 1)
			{
				msToCalibrate = 1;
			}
 
			// Loop until getTimer starts the next millisecond
			// The idea is to start as close to the beginning of the next
			// millisecond as possible, so the loop is kept as minimal and fast
			// as possible.
			var targetMS:int = getTimer() + 1;
			do
			{
			} while (getTimer() < targetMS);
 
			// Now that we are very close to the beginning of the millisecond we
			// are in a position to count the number of iterations we can
			// achieve in each subsequent millisecond.
			var iterationCount:int;
			targetMS = getTimer() + msToCalibrate;
			do
			{
				iterationCount++;
			} while (getTimer() < targetMS);
 
			// Average the number of iterations we achieved
			this.iterationsPerMS = Number(iterationCount) / msToCalibrate;
 
			// Return the number of iterations per millisecond in case it is of
			// interest to the caller
			return this.iterationsPerMS;
		}
 
		/**
		* Start the timer. The timer will wait until the beginning of
		* getTimer()'s next millisecond in order to establish a more accurate
		* start time. As a side effect, starting the timer can be costly in that
		* it could take up to one full millisecond to start. You should
		* therefore not start the timer often as it can easily degrade
		* application performance.
		* @return The next millisecond time, which will be the current time at
		*         the end of this function
		*/
		public function begin(): int
		{
			// Loop until getTimer starts the next millisecond
			// The idea is to start as close to the beginning of the next
			// millisecond as possible, so the loop is kept as minimal and fast
			// as possible.
			this.beginTime = getTimer() + 1;
			var targetMS:int = this.beginTime;
			do
			{
			} while (getTimer() < targetMS);
 
			// Now that we are very close to the beginning of the millisecond
			// the caller can begin the activities to time. Return them the
			// current time.
			return targetMS;
		}
 
		/**
		* End the timer and get the elapsed time since it began
		* @return The number of milliseconds elapsed since begin() last returned
		*         or a NaN if iterationsPerMS is either NaN or negative. If
		*         iterationsPerMS is negative, the negation of the correct value
		*         is returned.
		*/
		public function end(): Number
		{
			// Since we could be ending the timer at any point during the
			// millisecond, we need to count iterations in the same way as
			// during calibration until the millisecond ends in order to
			// determine the amount of the millisecond that occurred before the
			// call to this function.
			var iterationCount:int;
			var targetMS:int = getTimer() + 1;
			do
			{
				iterationCount++;
			} while (getTimer() < targetMS);
 
			// Compute the portion of the millisecond that came after the call
			// to this function based on the calibration value iterationsPerMS
			var portionNotUsed:Number = iterationCount / this.iterationsPerMS;
 
			// The traditional way to get the elapsed time would be
			//   (end - begin)
			// which in this case would be
			//   (targetMS - beginTime - 1)
			// However, that would not account for the portion of the
			// millisecond that was used after the "end" millisecond began. So
			// instead of subtracting the full millisecond ("1" in the above),
			// only subtract the portion of the millisecond that we've computed
			// was not used.
			return targetMS - this.beginTime - portionNotUsed;
		}
	}
}

We’ll get to the sample app soon, but first it was necessary to create a simple class to display a bar chart representation of the results since they are so subtle in text form. So I threw together the following class which you may find useful in your own projects if simple and dependency-free is what you’re after. Again, this was made quickly so it has no comments, minimal testing, and just enough features for the sample app:

package
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.display.Shape;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
 
	/**
	* A simple bar chart Sprite
	* @author Jackson Dunstan (http://JacksonDunstan.com)
	* @license MIT (http://opensource.org/licenses/MIT)
	*/
	public class SimpleBarChart extends Sprite
	{
		public function SimpleBarChart(
			columns:Array,
			width:Number,
			height:Number,
			title:String,
			borderColor:uint
		)
		{
			graphics.lineStyle(1, borderColor);
			graphics.drawRect(0, 0, width, height);
 
			var titleTF:TextField = new TextField();
			titleTF.autoSize = TextFieldAutoSize.LEFT;
			titleTF.text = title;
			titleTF.mouseEnabled = false;
			titleTF.x = (width - titleTF.width) / 2;
			titleTF.y = -titleTF.height;
			addChild(titleTF);
 
			var labels:Array = [];
			var maxLabelWidth:Number = Number.MIN_VALUE;
			for (var i:int; i < columns.length; ++i)
			{
				column = columns[i];
 
				var labelTF:TextField = new TextField();
				labelTF.autoSize = TextFieldAutoSize.LEFT;
				labelTF.text = column.label;
				var labelBMD:BitmapData = new BitmapData(
					labelTF.width,
					labelTF.height,
					true,
					0x00000000
				);
				labelBMD.draw(labelTF);
				var labelBM:Bitmap = new Bitmap(labelBMD);
				maxLabelWidth = Math.max(maxLabelWidth, labelBM.width);
				labels.push(labelBM);
			}
 
			var barAreaHeight:Number = height - maxLabelWidth;
 
			var maxValue:Number = Number.MIN_VALUE;
			for each (var column:Object in columns)
			{
				maxValue = Math.max(column.value, maxValue);
			}
 
			var maxDigits:int = String(int(maxValue)).length;
			var hashInt:int = Math.pow(10, maxDigits-1);
			const MAX_HASHES:int = 10;
			const HASH_WIDTH:Number = 20;
			var numHashes:int = Math.ceil(maxValue / hashInt) + 1;
			var hashSpacing:Number = barAreaHeight / (numHashes-1);
			var maxHashLabelWidth:Number = 0;
			var hashLabels:Array = [];
			for (i = 0; i < numHashes; ++i)
			{
				var hashTF:TextField = new TextField();
				hashTF.autoSize = TextFieldAutoSize.LEFT;
				hashTF.text = String(i*hashInt);
				hashTF.selectable = false;
				hashTF.mouseEnabled = false;
				hashLabels[i] = hashTF;
 
				maxHashLabelWidth = Math.max(maxHashLabelWidth, hashTF.width);
			}
			for (i = 0; i < numHashes; ++i)
			{
				var hash:Shape = new Shape();
				hash.graphics.lineStyle(1, 0xff000000);
				hash.graphics.lineTo(HASH_WIDTH, 0);
				hash.x = maxHashLabelWidth;
				hash.y = barAreaHeight - Number(i)*hashSpacing;
				addChild(hash);
 
				hashTF = hashLabels[i];
				hashTF.y = hash.y - (hashTF.height-hash.height)/2;
				addChild(hashTF);
			}
			var hashAreaWidth:Number = maxHashLabelWidth + HASH_WIDTH;
 
			var maxHashValue:Number = (numHashes-1) * hashInt;
			var maxBarHeight:Number = barAreaHeight * (maxValue/maxHashValue);
 
			var barWidth:Number = (width-hashAreaWidth) / columns.length;
			for (i = 0; i < columns.length; ++i)
			{
				column = columns[i];
 
				var barHeight:Number = maxBarHeight * (column.value/maxValue);
				var bar:Shape = new Shape();
				bar.graphics.beginFill(column.color);
				bar.graphics.drawRect(
					hashAreaWidth+barWidth*i,
					height-barHeight-maxLabelWidth,
					barWidth,
					barHeight
				);
				bar.graphics.endFill();
				addChild(bar);
 
				labelBM = labels[i];
				labelBM.x = hashAreaWidth + barWidth * i + barWidth/2;
				labelBM.y = height + labelBM.width - maxLabelWidth;
				labelBM.rotation = -90;
				addChild(labelBM);
			}
		}
	}
}

Finally, the sample app. This app simply compares the results using the traditional getTimer approach and the new Timely approach. Your computer’s calibration results are displayed at the top followed by CSV-formatted results and bar chart versions of both.

package
{
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.utils.getTimer;
 
	public class TestTimely extends Sprite
	{
		private var logger:TextField;
		private function row(...cols): void
		{
			logger.appendText(cols.join(",")+"\n");
		}
 
		public function TestTimely()
		{
			init();
		}
 
		private function init(): void
		{
			logger = new TextField();
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			const REPS:int = 2000000;
			const TESTS_EACH:int = 5;
			var i:int;
			var j:int;
			var test:int;
			var beginTime:int;
			var endTime:int;
			var elapsed:Number;
			var times:Array;
			var timely:Timely = new Timely();
 
			row("Iterations per MS: " + timely.iterationsPerMS);
			row();
 
			times = ["Timer"];
			for (test = 0; test < TESTS_EACH; ++test)
			{
				times.push("Test " + (test+1));
			}
			row.apply(this, times);
 
			times = ["Timely"];
			for (test = 0; test < TESTS_EACH; ++test)
			{
				timely.begin();
				for (i = 0; i < REPS; ++i)
				{
					j++;
				}
				elapsed = timely.end();
				times.push(elapsed);
			}
			row.apply(this, times);
			chartRow(times);
 
			times = ["getTimer"];
			for (test = 0; test < TESTS_EACH; ++test)
			{
				beginTime = getTimer();
				for (i = 0; i < REPS; ++i)
				{
					j++;
				}		   
				endTime = getTimer();
				times.push(endTime - beginTime);
			}
			row.apply(this, times);
			chartRow(times);
		}
 
		private var chartX:Number = 10;
		private function chartRow(row:Array): void
		{
			const COLORS:Array = [
				0xaf002a,
				0x387a57,
				0x007fff,
				0xffe135,
				0xbf94e4
			];
 
			var title:String = row.shift();
 
			var columns:Array = new Array(row.length);
			for (var i:int; i < row.length; ++i)
			{
				columns[i] = {
					label: "Test " + (i+1),
					color: COLORS[i],
					value: row[i]
				};
			}
			var chart:SimpleBarChart = new SimpleBarChart(
				columns,
				200,
				200,
				title,
				0xffcccccc
			);
			chart.x = chartX;
			chart.y = 100;
			addChild(chart);
 
			chartX += chart.width + 10;
		}
	}
}

Run the test app

I ran this test in the following environment:

  • Release version of Flash Player 11.8.800.97
  • 2.3 Ghz Intel Core i7
  • Mac OS X 10.8.4
  • ASC 2.0 build 353448 (-debug=false -verbose-stacktraces=false -inline)

And here are the results I got:

Timer Test 1 Test 2 Test 3 Test 4 Test 5
Timely 3.962480352887492 3.6190572766144435 3.69054741503152 3.8837228954351097 3.6829420811573628
getTimer 3 4 4 4 3

Performance Graph

Notice how the getTimer results are always waffling between 3 and 4 whole milliseconds while the Timely results only range between 3.619 and 3.962: a huge improvement. However, there is one downside to using Timely. In addition to several milliseconds of calibrating, it also requires up to a whole millisecond to start the timer and up to a whole millisecond to stop the timer. This overhead is extreme compared to the negligible time that simple getTimer calls take, so use Timely only in performance test apps or sparingly in production code.

I’m considering switching all future tests on this site to using Timely. What do you think? Let me know in the comments and especially if you find any bugs in Timely or have any suggestions to improve the accuracy of timing AS3 code.