ActionScript Timer class seem offset/inaccurate/way off? It is!

November 1, 2008 at 10:20 pm • 1 Comment

Something I discovered a while ago has popped up in one of my old projects and I decided to share what I found. My Virtual Cube application, which renders 3D interactive virtual Rubik’s cubes, uses ActionScript’s Timer class to create a timer to keep track of how long it takes a user to solve a scrambled cube. While using the application, I noticed the timer running slower while using the 5×5x5 cube and faster with the 2×2x2 cube. What would be three seconds while using the small cube would be six or seven seconds for the large cube.

What was happening was the frame rate was slowing down because the Sandy 3D engine (an ActionScript 3D engine I use to render the 3D Rubik’s cubes) was taking longer to render the more complex (5×5x5) cube and all of its “cubies” that rotate. But what makes it rather odd was that this slowdown in frame rate was affecting the timer the user saw in the application. Because Flash had to work harder, and longer, to display the more complex cube every frame it would take that much longer for my TimerEvent.TIMER event to dispatch and update my timer in the user interface.

To put it in a nutshell, the Timer class is not what you think it is (a perfect timer); it is actually influenced by the user’s frame rate. Let’s take a look at what it says right in the documentation that can be found in the Flash Help book within Flash CS or at the online Adobe® Flex™ 2 Language Reference:

You can create Timer objects to run once or repeat at specified intervals to execute code on a schedule. Depending on the SWF file’s framerate or Flash Player’s environment (available memory and other factors), Flash Player may dispatch events at slightly offset intervals. For example, if a SWF file is set to play at 10 frames per second [fps], which is 100 millisecond intervals, but your timer is set to fire an event at 80 milliseconds, Flash Player will fire the event close to the 100 millisecond interval. Memory-intensive scripts may also offset the events.

Tom Coxen has reported this “bug” a while ago and released some information regarding it. Below is some code he posted to reproduce the bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Timer inacurracy example.
* See: https://bugs.adobe.com/jira/browse/FP-34
*
* Originally by Tox Coxen - http://flashquartermaster.com
* Slight modifications by Dan - http://thewebpageofdan.com
*/
package
{
	import flash.display.MovieClip;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.utils.getTimer;
 
	public class TimerTest extends MovieClip
	{
		private var _start:uint;
   		private var _desiredDelay:Number = 1000;
 
		public function TimerTest()
		{
			var timer:Timer = new Timer(_desiredDelay);
			timer.addEventListener(TimerEvent.TIMER, tick);
			_start = getTimer();
			timer.start();
		}
 
		private function tick(evt:TimerEvent):void
		{
			var timer:Timer = Timer(evt.currentTarget);
			var elapsed:Number = (getTimer() - _start);
			var desiredElapsed:Number = (timer.currentCount * _desiredDelay);
			var offset:Number = elapsed - desiredElapsed;
 
			if (offset > 1000)
			{
				trace("MORE THAN A SECOND OFF!");
			}
			trace("Tick | Count: " + timer.currentCount + " / desired Elpased millis " + desiredElapsed + " / getTimer() elapsed millis: " + elapsed + " / offset: " + offset);
		}
	}
}

Here are the results I get from running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Tick | Count: 1 / desired elapsed millis 1000 / getTimer() elapsed millis: 1059 / offset: 59
Tick | Count: 2 / desired elapsed millis 2000 / getTimer() elapsed millis: 2112 / offset: 112
Tick | Count: 3 / desired elapsed millis 3000 / getTimer() elapsed millis: 3175 / offset: 175
Tick | Count: 4 / desired elapsed millis 4000 / getTimer() elapsed millis: 4247 / offset: 247
Tick | Count: 5 / desired elapsed millis 5000 / getTimer() elapsed millis: 5311 / offset: 311
Tick | Count: 6 / desired elapsed millis 6000 / getTimer() elapsed millis: 6390 / offset: 390
Tick | Count: 7 / desired elapsed millis 7000 / getTimer() elapsed millis: 7463 / offset: 463
Tick | Count: 8 / desired elapsed millis 8000 / getTimer() elapsed millis: 8511 / offset: 511
Tick | Count: 9 / desired elapsed millis 9000 / getTimer() elapsed millis: 9559 / offset: 559
Tick | Count: 10 / desired elapsed millis 10000 / getTimer() elapsed millis: 10607 / offset: 607
Tick | Count: 11 / desired elapsed millis 11000 / getTimer() elapsed millis: 11671 / offset: 671
Tick | Count: 12 / desired elapsed millis 12000 / getTimer() elapsed millis: 12687 / offset: 687
Tick | Count: 13 / desired elapsed millis 13000 / getTimer() elapsed millis: 13689 / offset: 689
Tick | Count: 14 / desired elapsed millis 14000 / getTimer() elapsed millis: 14759 / offset: 759
Tick | Count: 15 / desired elapsed millis 15000 / getTimer() elapsed millis: 15767 / offset: 767
Tick | Count: 16 / desired elapsed millis 16000 / getTimer() elapsed millis: 16823 / offset: 823
Tick | Count: 17 / desired elapsed millis 17000 / getTimer() elapsed millis: 17871 / offset: 871
Tick | Count: 18 / desired elapsed millis 18000 / getTimer() elapsed millis: 18918 / offset: 918
Tick | Count: 19 / desired elapsed millis 19000 / getTimer() elapsed millis: 19927 / offset: 927
Tick | Count: 20 / desired elapsed millis 20000 / getTimer() elapsed millis: 20983 / offset: 983
MORE THAN A SECOND OFF!
Tick | Count: 21 / desired elapsed millis 21000 / getTimer() elapsed millis: 22038 / offset: 1038
MORE THAN A SECOND OFF!
Tick | Count: 22 / desired elapsed millis 22000 / getTimer() elapsed millis: 23119 / offset: 1119

What this demonstrates is that the intervals at which TimerEvent.TIMER events occur can become inaccurate and increasingly inaccurate over time. It’s definitely in Adobe’s best interest to come up with a way to ensure that the the Timer class’s events dispatch at their correct intervals because of how misleading it is to Flash developers using ActionScript 3.0, even though it’s stated clearly in the documentation. Wouldn’t you think a class called “Timer” would work as expected and dispatch it’s ticks at even timed intervals? Luckily, Tom posted an ActionScript class with his bug report to use as a workaround, not a complete solution (see note below). A modified version, which I now use on all my Flash projects that require a timer, is posted below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* Workaround for Timer offsets
* See: https://bugs.adobe.com/jira/browse/FP-34
*
* Originally by Tox Coxen - http://flashquartermaster.com
* Edited by Dan - http://thewebpageofdan.com
*/
package
{
	import flash.utils.Timer;
	import flash.utils.getTimer;
 
	public class TimerReal extends Timer
	{
		private var _desiredDelay:Number;
		private var _initStart:Number;
   		private var _stop:Number;
   		private var _start:Number;
   		private var _pauses:Number;
		private var _offset:Number;
 
		/**
		* Constructs a new RealTimer object with the specified <code>delay</code> and <code>repeatCount</code> states. 
		* 
		* @param delay		The delay between timer events, in milliseconds. 
		* @param repeatCount	Specifies the number of repetitions. If zero, the timer repeats infinitely. If nonzero, the timer runs the specified number of times and then stops. 
		*/
		public function TimerReal(delay:Number, repeatCount:int = 0.0)
		{
			_desiredDelay = delay;
			_initStart = 0;
			_start = 0;
			_stop = 0;
			_pauses = 0;
			super(delay, repeatCount);
		}
 
		/**
		* Manages the timer drift and accounts for pauses caused by stopping and starting the Timer.
		*/
		public function adjustDelay():void
		{
			// Aggregate pauses.
			// When start and stop are called internally in flash.utils.Timer _start will equal _stop
			_pauses += _start - _stop;
 
			var elapsed:Number = (getTimer() - _initStart) - _pauses;
			var desiredElapsed:Number = (currentCount * _desiredDelay);
			_offset = elapsed - desiredElapsed;
			var newDelay:Number = _desiredDelay - _offset;
 
			// Setting negetive values for delay will cause an error
			delay = (newDelay > 0) ? newDelay : _desiredDelay;
		}
 
		/**
		* @private
		*/
		override public function start():void
		{
			// Set _initStart once only otherwise set a value for when Timer.start() is called
			if (_initStart == 0)
			{
				_initStart = getTimer();
			}
			else
			{
				_start = getTimer();
			}
			super.start();
		}
 
		/**
		* @private
		*/
		override public function stop():void
		{
			// Only set the stop value if the Timer has been started and needs to be stopped i.e. is running
			if (_initStart != 0 && running)
			{
				_stop = getTimer();
			}
			super.stop();
		}
 
		/**
		* @private
		*/
		override public function reset():void
		{
			_initStart = 0;
			_start = 0;
			_stop = 0;
			_pauses = 0;
			super.reset();
		}
 
		/**
		* The desired delay.
		*/
		public function set desiredDelay(value:Number):void
		{
			_desiredDelay = value;
		}
 
		/**
		* The current offset.
		*/
		public function get offset():Number
		{
			return _offset;
		}
	}
}

The TimerReal class uses the getTimer() function to correct the delay property as well as the new offset property, which can be used help determine how much actual time has elapsed between intervals. Here is an example of how you could use the class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* TimerReal example.
* By Dan - http://thewebpageofdan.com
*/
package
{
	import flash.display.MovieClip;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.utils.getTimer;
 
	public class TimerRealExample extends MovieClip
	{
		public function TimerRealExample()
		{
			var timer:TimerReal = new TimerReal(1);
			timer.addEventListener(TimerEvent.TIMER, tick);
			timer.start();
		}
 
		private function tick(evt:TimerEvent):void
		{
			TimerReal(evt.currentTarget).adjustDelay();
			var timeStr:String = millisToTimeStr(evt.target.currentCount + evt.target.offset);
			trace(timeStr);
		}
 
		/**
		* Converts milliseconds to a time string
		*/
		private function millisToTimeStr(milliseconds:uint):String
		{
			var seconds:uint = Math.floor(milliseconds / 1000);
			var minutes:uint = Math.floor(seconds / 60);
			var hours:uint = Math.floor(minutes / 60);
 
			var lminutes:uint = minutes - (hours * 60);
			var lseconds:uint = seconds - (minutes * 60);
			var lmilliseconds:uint = milliseconds - seconds * 100;
 
			var millisecondsStr:String = (lmilliseconds >= 10) ? String(lmilliseconds) : "0" + lmilliseconds;
			var secondsStr:String = (lseconds >= 10) ? String(lseconds) : "0" + lseconds;
			var minutesStr:String = (lminutes >= 10) ? String(lminutes) : "0" + lminutes;
			var hoursStr:String = (hours >= 10) ? String(hours) : "0" + hours;
 
			return String(((hours) ? hoursStr + ":" : "") + minutesStr + ":" + secondsStr + "." + millisecondsStr);
		}
	}
}

Note: It’s important to remember that the TimerEvent.TIMER event will still dispatch at incorrect intervals.

So basically, if you need a certain process to happen at a timer interval (i.e. every 30 seconds), the Timer class cannot be depended on to make it happen at the exact desired interval. But if your using it to update a visible timer in a user interface, this “fix” will allow you to adjust an incorrect value that would normally be shown to the user. If anyone dives deeper into this, let me know what you find!

1 Comment »

  1. thank you for the information…

    [Reply]

    Comment by egdo — November 3, 2008 @ 4:38 pm

Comments RSS feed. TrackBack URL

Leave a comment