Monday, April 14, 2008

Time's Up

In trying to figure out the wacky results I was seeing from running the BubbleMark application, I discovered that timer events are constrained in Flash (and therefore Flex) when running inside the browser. This limitation extends to all methods of timing, including: the Timer class, handling ENTER_FRAME events, and manually calling callLater().

Meanwhile, using these timing methods outside of the browser, either in the standalone Flash player or in an AIR application, returns very different results because Flash is no longer constrained by the browser's throttling.

I wrote a simple application, TimerBenchmark, to show the differences between the different timing mechanisms inside the browser and out. The user selects the timing mechanism desired, optionally enters a resolution value (the milliseconds between timing events) for the Timer case, and the test automatically runs. As the test runs, it counts how many timing events are executed per second and displays that result in the "FPS" field at the top of the application. For example, here's the application, runnin in your browser:

How it Works

First, let's see the code. Everything is in the TimerBenchmark MXML file, which you can download and play with. The GUI is set up with a combination of labels, radio buttons, and a TextInput control for the Timer resolution:

    <mx:Label id="fps" x="75" y="48"/>
    <mx:Label x="10" y="48" text="FPS"/>
    <mx:Label x="114" y="76" text="Timer resolution"/>
    <mx:TextInput x="218" y="74" width="39" id="resolution"
        enabled="{timerButton.selected}" text="1" enter="restart()"/>
    <mx:RadioButtonGroup id="benchmarkType" change="restart()"/>
    <mx:RadioButton id="timerButton" x="10" y="74" label="Timer" groupName="benchmarkType"/>
    <mx:RadioButton id="enterFrameButton" x="10" y="100" label="enterFrame" groupName="benchmarkType"/>
    <mx:RadioButton id="callLaterButton" x="10" y="126" label="callLater" groupName="benchmarkType"/>

Any change in the Timer resolution input or one of the radio buttons will call restart(), which will reset any running benchmark and then execute the appropriate test:

            private function restart():void
            {
                if (timer)
                    timer.stop();
                removeEventListener(Event.ENTER_FRAME, enterFrame);
                callLaterRunning = false;
                runBenchmark();
            }
            public function runBenchmark():void
            {
                switch (benchmarkType.selection) {
                    case timerButton:
                       startTimerTest();
                       break;
                    case enterFrameButton:
                       startEnterFrameTest();
                       break;
                    case callLaterButton:
                       startCallLaterTest();
                       break;
                }
            }

All of the tests call benchmarkCallback() to do the actual work of timing the mechanisms:

            private function benchmarkCallback():void
            {
                ++numFrames;
                var nowTime:int = getTimer();
                var delta:int = (nowTime - startTime);
                if (delta > 1000)
                {
                    fps.text = String(numFrames * (delta / 1000));
                    numFrames = 0;
                    startTime = nowTime;
                }
                if (callLaterRunning)
                    callLater(benchmarkCallback);
            }

benchmarkCallback() counts the number of times that it's been called since the last measurement time. Every second, it calculates the fps result, based on numFrames and the time elapsed since the 0th frame. It updates the fps label appropriately and then resets numFrames and startTime. Note that our benchmark function doesn't actually do anything useful. It's a bit like our politician, except that runBenchmark() is intentially unproductive because we only want to time how fast the timing mechanisms can run, not how fast we process anything when they get there. It's important to be able to disentangle these concepts from each other, lest we run into the confusion I encountered in the BubbleMark program, where the throttled rate of Timer was affecting the perceived computation or rendering performance of the application.

startTimerTest() is the function called when the Timer button is pressed:

            public function startTimerTest():void
            {
                if (timer && timer.running)
                    timer.stop();
                timer = new Timer(Number(resolution.text));
                timer.addEventListener(TimerEvent.TIMER, timerEvent);
                timer.start();
            }
            private function timerEvent(event:TimerEvent):void
            {
                benchmarkCallback();
            }

This mechanism works by asking the Flash runtime to call our callback function, timerEvent(), every n milliseconds, where we set n to be equal to the value in our "Timer resolution" text input control.

The enterFrame mechanism looks like this:

            private function startEnterFrameTest():void {
                addEventListener(Event.ENTER_FRAME, enterFrame);
            }
            public function enterFrame(event:Event):void
            {
                benchmarkCallback();
            }

This mechanism works by telling the Flash player to call our callback function, enterFrame(), whenever the ENTER_FRAME event is processed (which is, by definition, once per 'frame' that Flash renders).

Finally, the callLater() benchmark is run by the following code:

            private function startCallLaterTest():void
            {
                callLaterRunning = true;
                benchmarkCallback();
            }

in addition to the final two lines of our benchmarkCallback() function:

                if (callLaterRunning)
                    callLater(benchmarkCallback);

This mechanism works by asking the Flash runtime to call our function, benchmarkCallback(), at the next available opportunity, which occurs twice per frame.

Resolution Results

Here are some sample results with the application running inside the browser on my Windows Vista system:

As you can see, the Timer and EnterFrame mechanisms are both throttled at about 65 frames per second. Meanwhile, the callLater mechanism is getting serviced at about twice that rate.

The reason for the throttling is that the timing system that Flash uses inside the browser is pegged at a maximum, which is a combination of a limitation of the timing mechanism of the browser itself and the refresh rate of the monitor (which for my laptop is 60 fps). The main reason for this limitation is to avoid pegging the CPU just to run animations that really don't need to run faster than the monitor's refresh rate. After all, why waste time and CPU resources updating something that the user will only see updated at the refresh rate of the monitor?

Both the Timer mechanism and enterFrame are maxed out to this same limit. The callLater mechanism sees about twice this rate because it is actually called exactly twice per frame in Flash. It is called once when leaving the enterFrame() call and another when processing the actual frame rendering, so an application that requests a callback through callLater can expect to get that callback twice per frame.

Meanwhile, here are some results from running in the standalone Flash player:

Now we can see that the Timer approach gets close to our ideal fps number; since the resolution was set at 1 and we set the internal Flash frameRate property to 1000, the ideal result is 1000 frames per second. The enterFrame mechanism seems to be throttled at a much lower 250 fps, although this is still significantly better than the browser limit of 65 fps. The callLater mechanism is again twice that of enterFrame, which is what we would expect.

I wrote an AIR version of the benchmark as well. I won't bother to post that version, since I got the same results as the standalone Flash player. This is as expected, since the AIR version is basically running the Flash player standalone internally, freed from the browser constraints. So it is essentially equivalent to the standalone Flash player version.

Resolution Resolution

So where does that leave things? What are we to make of the different timing results?

Well, we mainly need to be aware of the issues, especially when running timing-sensitive code. What mechanism should you use? What frame rate do you want? Are you really getting the results you think you are, or is there some environmental effect that is having an impact on the timing of your application?

In most real-world situations, your application need not (and probably should not) run at a faster rate than the resolution of the screen, so the limitation of the timing mechanisms to refresh rates is perfectly reasonable. In particular, in graphical applications any updates to the scene at greater than refresh rates is simply a waste of cycles, since the user won't see that many updates. But if you need to run some function at greater than this rate for some reason, be aware of the factors called out above and maybe even do some benchmarking of your own to make sure you're getting what you think you're getting.

For More Information

Here's my previous post on Timer's effect on the BubbleMark results

Here's James Ward's post on the same topic, with some alternate benchmark applications and results

Here'sa blog post by Adobe's Tinic Uro on the throttled frame rate in Flash

Here's the original BubbleMark site

Here's the source code for my application, TimerBenchMark

Here's the Flash SWF file for the benchmark

5 comments:

John Dowdell said...

Yup, this is true, but it's a hard meme to raise above the noise. Mac Projectors usually do much better than Mac browsers on identical content. But "slow on Mac" has gotten more traction than "test on the desktop".

Differences in hosting resources shouldn't affect a benchmark much, but benchmarks vary so much, that it might.... ;-)

Another tough meme has been that one you raise about optimal screen rate... even though Disney animated 12-up, and film is at 24fps, the ability to request high framerates (and increase processor load) has been the common fixation.

Thanks for putting hard evidence behind it all, though.... ;-)

jd/adobe

Cédric Néhémie said...

Hi,

Thanks for sharing that. Just a word to say that the problem of the Flash Player's framerate lost in a browser is rather old now, I remember several post from Andre Michelle (http://blog.andre-michelle.com/2005/tight-fps-solution/ for example, which will have three years old soon) about that point, and see many approach to keep the framerate stable (using transparent flash, infinite loop, etc...).

Instead trying to keep the player framerate at a constant speed, which is IMHO a bad idea when we see how ustable is the flash player behavior (the same animation running on windows in a browser at 40fps, run with difficulty at 20fps in the standalone player on my linux).

That why I wrote Kairos one year ago, a small framework which avoid the use of any hack to make the player framerate stable by the use of discrete computation for all animated elements (movie clips, tweens, etc...) based on the recorded time between each "frames". If I can suggest you to take a look at it :
http://code.google.com/p/kairos3/

Chet Haase said...

Cédric,

Thanks for the information and links. Note, however, that I was only talking about the average frame rate. I haven't gotten around to measuring the variability of the inter-frame rate. That's a fairly separate (albeit related and important) timing issue.

Matt Perkins said...

i'm viewed this in firefox 3 rc1 on XP and saw numbers like you saw in the stand alone player. maybe it's more of an IE thing?

Tek said...

@matt> It seems that FF3 do not limit the framerate as FF2 does on windows. I've done some benchmarks and give the results here : http://www.tekool.net/blog/2008/05/27/overriding-flash-player-60fps-limit-in-firefox-up-to-950fps-as-silverlight-2-in-bubblemark/