Since Flash Player 11.4 was released we have finally been given the ability to run multiple threads of AS3 code to take advantage of modern multi-core CPUs. However, when we start writing this multi-threaded code we immediately run into the requirement to coordinate the threads by passing messages between them. As it turns out, this is quite slow in AS3. Read on for the performance analysis.

The following test app performs a very simple task in two ways. The main thread passes a series of integers to a worker thread and the worker thread adds those integers together to compute their sum. Two ways of accomplishing this task are used. The first way passes 1000 individual messages to the worker thread, each with an integer to add. A final message is passed to tell the worker to pass the sum back to the main thread and clear for the next test. The second way of computing the sum creates a ByteArray with the shareable flag set to true. This ByteArray is then filled with all of the integers to sum and sent to the worker thread. The worker thread extracts these integers, adds them together, and passes a message back with the sum.

These two ways have been designed such that one passes a lot of messages and the other only passes one. Check out the source code for details.

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.utils.getTimer;
	import flash.utils.ByteArray;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.system.Worker;
	import flash.system.WorkerDomain;
	import flash.system.WorkerState;
	import flash.system.MessageChannel;
 
	public class MessageChannelTest extends Sprite
	{
		private var logger:TextField = new TextField();
		private function row(...cols): void
		{
			logger.appendText(cols.join(",")+"\n");
		}
 
		private var mainToWorker:MessageChannel;
		private var mainToWorkerBytes:MessageChannel;
		private var mainToWorkerClear:MessageChannel;
		private var workerToMain:MessageChannel;
		private var worker:Worker;
 
		private var REPS:int = 100000;
		private var beforeTime:int;
		private var sum:int;
		private var correctSum:int;
 
		public function MessageChannelTest()
		{
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			if (Worker.current.isPrimordial)
			{
				startMainThread();
			} 
			else
			{
				startWorkerThread();
			}
		}
 
		private function startMainThread(): void
		{
			worker = WorkerDomain.current.createWorker(this.loaderInfo.bytes);
 
			mainToWorker = Worker.current.createMessageChannel(worker);
			worker.setSharedProperty("mainToWorker", mainToWorker);
 
			mainToWorkerBytes = Worker.current.createMessageChannel(worker);
			worker.setSharedProperty("mainToWorkerBytes", mainToWorkerBytes);
 
			mainToWorkerClear = Worker.current.createMessageChannel(worker);
			worker.setSharedProperty("mainToWorkerClear", mainToWorkerClear);
 
			workerToMain = worker.createMessageChannel(Worker.current);
			workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMainDirect);
			worker.setSharedProperty("workerToMain", workerToMain);
 
			worker.start();
 
			for (var i:int = 0; i < REPS; ++i)
			{
				correctSum += i;
			}
 
			row("Type", "Time", "Correct?");
 
			sum = 0;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				mainToWorker.send(i);
			}
			mainToWorkerClear.send(true);
		}
 
		private function startWorkerThread(): void
		{
			mainToWorker = Worker.current.getSharedProperty("mainToWorker");
			mainToWorkerBytes = Worker.current.getSharedProperty("mainToWorkerBytes");
			mainToWorkerClear = Worker.current.getSharedProperty("mainToWorkerClear");
			workerToMain = Worker.current.getSharedProperty("workerToMain");
 
			mainToWorker.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorker);
			mainToWorkerBytes.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorkerBytes);
			mainToWorkerClear.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorkerClear);
		}
 
		private function onMainToWorker(event:Event): void
		{
			sum += mainToWorker.receive();
		}
 
		private function onMainToWorkerBytes(event:Event): void
		{
			var bytes:ByteArray = mainToWorkerBytes.receive();
			bytes.position = 0;
			var numInts:int = bytes.length / 4;
 
			var sum:int;
			for (var i:int; i < numInts; ++i)
			{
				sum += bytes.readInt();
			}
 
			workerToMain.send(sum);
		}
 
		private function onMainToWorkerClear(event:Event): void
		{
			workerToMain.send(sum);
			sum = 0;
		}
 
		private function onWorkerToMainDirect(event:Event): void
		{
			sum = workerToMain.receive();
 
			var afterTime:int = getTimer();
			row("Direct", (afterTime-beforeTime), (sum==correctSum));
 
			workerToMain.removeEventListener(Event.CHANNEL_MESSAGE, onWorkerToMainDirect);
			workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMainBytes);
 
			var bytes:ByteArray = new ByteArray();
			bytes.shareable = true;
			bytes.length = REPS*4;
			bytes.position = 0;
 
			sum = 0;
			beforeTime = getTimer();
			for (var i:int; i < REPS; ++i)
			{
				bytes.writeInt(i);
			}
			mainToWorkerBytes.send(bytes);
		}
 
		private function onWorkerToMainBytes(event:Event): void
		{
			sum = workerToMain.receive();
 
			var afterTime:int = getTimer();
			row("ByteArray", (afterTime-beforeTime), (sum==correctSum));
		}
	}
}

Run the test

I ran this test in the following environment:

  • Release version of Flash Player 11.8.800.170
  • 2.3 Ghz Intel Core i7
  • Mac OS X 10.8.5
  • ASC 2.0.0 build 353981 (-debug=false -verbose-stacktraces=false -inline -optimize=true)

And here are the results I got:

Type Time
Direct 2359
ByteArray 4

Performance Chart

The ByteArray version is clearly leaps and bounds faster than the direct version that passes individual messages. In this test environment at least, each message is taking 0.02359 milliseconds. A mere 50 messages would eat up a whole millisecond of this relatively-fast CPU, probably far too much for a complex game that’s otherwise busy with 3D graphics, physics, and so forth. If you can, it’s much quicker to bundle up these messages into a ByteArray marked shareable and send them all over at once.

Spot a bug? Have a question or suggestion? Post a comment!