ActionScript 3.0 Draw Speed Test: Drawing Primitives Vs. Placing MovieClips

I got a comment about ActionScript 3 the other day which asked a simple and fair question (paraphrased as):

If you’re just using simple shapes like circles, is it faster to draw them to the stage with drawCircle, or to use MovieClips/Sprites to draw them?

Did I know the answer? Um, no… So I knocked up a quick bit of flash to find out. Before you’re given the answer, why not have a quick ponder about what you think will happen? I’ve got to admit, when I did this I got completely the wrong result! Ha! Shows what I know about anything…

Brief analysis plus source-code n’ file after the jump…

Well, who’d have thunk it?: Drawing is definitely quicker – but by how much? I’ve ran the test a bunch of times on (notoriously slow) 64-bit Linux Flash Plugin 10.0.42, and came in with numbers between 38% quicker and 125% quicker (i.e. drawing primitives can get the job done over twice as fast as placing MovieClips…). There really is quite a huge range in execution time, but drawing primitives is definitely the faster by far. Also, I showed the lovely wife what I’d spent a couple of hours yesterday doing, and when running on 64-bit Windows 7, results came back showing drawing to be over 400% faster, because the movie clip duration was over 1000 ticks! Shocking!

Typical Flash Speed Test Results on 64-bit Linux
Typical results on my 64-bit Linux rig: What numbers do you get?

I guess the MovieClip has the overhead of having a lot of other properties and things, and you have to take scaling into account… I’d originally thought that the drawn circles would be a little slower because you have to fill them in (which would mean you need to calc the area of the circle and go pixel by pixel), while the MovieClip might act more as stamp (which would mean adding the data to the screen by just dumping the bitmap data onto the stage), but I guess not…

It’s interesting to see that if you remove the MovieClip colour transforms, then drawing the circles is still significantly faster, as the colour transforms don’t take up a lot of CPU time at all…

Guess you learn something new every day! :D (Oh, and the whole Japanese motif is because while writing this I kept thinking about Banzai!).

Flash File Source Code:

// Program: Test out how long it takes to draw circles vs place circles as movie clips
// Author: r3dux
// Date: 12/02/2010
 
// Define our globals
var drawnCircleArray:Array = new Array;
var mcCircleArray:Array = new Array;
var tf:TextField = new TextField;
var numCircles:uint = 5000;
var drawDuration:uint;
var mcDuration:uint;
 
// Pre-calc'd random number setup
var randArray = new Array();
var randElement:uint = 0;
var randCount:uint = 5000;
initialiseRandArray();
 
// Does what it says on the tin.
function initialiseRandArray():void
{
	for (var i:uint = 0; i < randCount; i++)
	{
		randArray.push(Math.random());
	}
}
 
// Function to return a random number from our random number array
function getRandNumber():Number
{
	if (randElement < randCount)
	{
		randElement++;
	}
	else
	{
		randElement = 0;
	}
	return randArray[randElement];
}
 
// Function to generate a random hex colour then return it as uint
// Bit pointless really, could just return a random large uint, but
// good to know we can convert from String to uint should we ever need to...
function getRandHexColour():uint
{
	var hexColour:String = "0x"; // Zero x -not- letter-O x!!
 
	for (var loop:uint = 1; loop <= 6; loop++)
	{
		var randNum = Math.round(getRandNumber() * 15);
 
		switch (randNum)
		{
			case 10: 
				hexColour += "A";
				break;
			case 11: 
				hexColour += "B";
				break;
			case 12: 
				hexColour += "C";
				break;
			case 13: 
				hexColour += "D";
				break;
			case 14: 
				hexColour += "E";
				break;
			case 15: 
				hexColour += "F";
				break;
			default:
				hexColour += String(randNum);
				break;
		} // End of switch statement
 
	} // End of loop
 
	// Cast end end result to an unsigned integer and return it
	return uint(hexColour);
}
 
// Function to return us a random number within a specified range.
// Uses our array of random numbers to try to lower CPU load
function randRange(low:Number, high:Number):Number {
	return (getRandNumber() * (high - low)) + low;
} 
 
// Function to create and draw a circle
function addDrawnCircle(x:Number, y:Number, colour:uint = 15, radius:Number = 20):void
{
	var circle:Sprite = new Sprite;
	circle.graphics.beginFill(colour);
	circle.graphics.drawCircle(x, y, radius);
	circle.graphics.endFill();
 
	// Add our circle to the array
	drawnCircleArray.push(circle);
 
	// Place the circle on the screen by adding the last element of the array
	// Subtract 1 from length because arrays always start at element 0!
	stage.addChild(drawnCircleArray[drawnCircleArray.length-1]);
}
 
// Function to draw a circle from our movie clip
function addMovieClipCircle(xVal:Number, yVal:Number):void
{
	var circle:Circle = new Circle();
	circle.x = xVal;
	circle.y = yVal;
 
	// When drawing circles we can specify a colour, when using movie clips
	// we have to perform a colour transform to change the colour of the clip
	// to get the same effect
	var myColourTransform:ColorTransform = new ColorTransform;
	myColourTransform.redOffset   = randRange(-100, 150);
	myColourTransform.greenOffset = randRange(-100, 150);
	myColourTransform.blueOffset  = randRange(-100, 150);
	circle.transform.colorTransform = myColourTransform;
 
	mcCircleArray.push(circle);
 
	stage.addChild(mcCircleArray[mcCircleArray.length-1]);
}
 
// Slap the circles on the stage along with the timing results
function drawCircles(useMovieClip:Boolean):void
{	
	// Get the start time
	var startTime:uint = getTimer();
 
	// Draw however many circles we've specified
	if (useMovieClip == false)
	{
		for (var i:uint = 1; i <= numCircles; i++) {
			addDrawnCircle(randRange(20, 180), randRange(20, 380), getRandHexColour());
		}
	}
	else
	{
		for (var j:uint = 1; j <= numCircles; j++) {
			addMovieClipCircle(randRange(370, 530), randRange(20, 380));
		}
	}
 
	// Get the end time
	var endTime:uint = getTimer();
 
	// Calc the duration
	var duration:uint = endTime - startTime;
 
	if (useMovieClip == false)
	{
		drawDuration = duration;
	}
	else
	{
		mcDuration = duration;
	}	
 
	// Draw our text field
	tf.textColor = 0x000000;
	tf.x = 205;
	tf.y = 100;
	tf.selectable = false;
	tf.backgroundColor = 0x555555;
	tf.autoSize = TextFieldAutoSize.LEFT;
 
	// Let's see the numbers...
	if (useMovieClip == false) {
		tf.text = "Circle count is: " + numCircles + "\n\n";
		tf.appendText("Drawing primitives (Left):\n");
		tf.appendText("Started at: " + startTime + "\n");
		tf.appendText("Completed at: " + endTime + "\n");
		tf.appendText("Duration: " + drawDuration + " ticks\n");
	}
	else
	{
		tf.appendText("\nUsing movie clips (Right):\n");
		tf.appendText("Started at: " + startTime + "\n");
		tf.appendText("Completed at: " + endTime + "\n");
		tf.appendText("Duration: " + mcDuration + " ticks\n\n");
		tf.appendText("Drawing is faster by: " + Math.round(((mcDuration / drawDuration) - 1) * 100) + "%\n\n");
		tf.appendText("Click to re-run the test.");
	}
 
	// Add the textField to the stage
	stage.addChild(tf);
}
 
// Function to clean things up before re-running the test
function cleanUp():void
{
	// Remove old results textbox
	stage.removeChild(tf);
 
	//trace("Stage (before cleanup):" + stage.numChildren);
 
	// Remove all circles from the stage
	for (var i:uint = 0; i < numCircles; i++) {
		stage.removeChild(drawnCircleArray[i]);
		stage.removeChild(mcCircleArray[i]);
	}
 
	//trace("Stage (after cleanup):" + stage.numChildren);
 
	//trace("drawnCircleArray length before cleanup: " + drawnCircleArray.length);
	//trace("mcCircleArray length before cleanup: " + mcCircleArray.length);
 
	// Blank both arrays	
	drawnCircleArray.splice(0);
	mcCircleArray.splice(0);
 
	//trace("drawnCircleArray length after cleanup: " + drawnCircleArray.length);
	//trace("mcCircleArray length after cleanup: " + mcCircleArray.length);
}
 
var banner:StartBanner = new StartBanner;
banner.x = stage.stageWidth / 2;
banner.y = stage.stageHeight / 2;
stage.addChild(banner);
 
// Flag to control banner and sound on first run
var firstRun:Boolean = true;
 
// Run the test whenever the mouse is clicked
stage.addEventListener(MouseEvent.CLICK, runTest);
 
function runTest(e:MouseEvent):void
{
	if (firstRun == true)
	{
		stage.removeChild(banner);
	}
	else
	{
		// Call our cleanUp function then...
		cleanUp();
	}
 
	// ...put the circles on the screen...
	drawCircles(false); // ...by drawing them,...
	drawCircles(true);  // ...and then by using movie clips.
 
	// On first run, sound the gong!
	if (firstRun == true)
	{
		firstRun = false;
		var gong:GongSound = new GongSound();
		var channel:SoundChannel = gong.play();		
	}
}

The flash CS4 .fla file for the above can be found: here.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.