We know that sending messages between ActionScript workers is slow, but how can we make it faster? Today’s article discusses an alternative approach to message passing that yields a 2.5x speedup. Read on to learn about the approach and un-block your workers.

The ActionScript Worker Message Passing is Slow article used the MessageChannel class to send messages between threads. This is the natural way to communicate between them that is shown in much of the documentation on inter-thread communication. It provides an easy method to communicate between workers that looks and feels exactly like the rest of the Flash API’s Event-based system. If you want it to be even easier, you can use MessageComm.

However, there is a downside to all of this convenience. MessageChannel has a high level of performance overhead. The biggest area of performance overhead is that all messages are put into a queue and delivered once per frame, just like other events such as COMPLETE and ENTER_FRAME. This effectively limits the frequency at which your threads can communicate to once per frame. A slower frame rate will reduce your thread communication frequency, which may compound the low frame rate problem.

Luckily, there is an alternative way to pass messages between workers. It’s actually contained in the setup code used to establish the MessageChannel link between the threads: setting shared properties. Instead of calling Worker.setSharedProperty just once before the worker thread is started, you instead set it every time you have a message to pass to another thread. Think of it like a drop box. One thread places a message there and the other thread periodically checks (with Worker.getSharedProperty) to see if there is a message. You can indicate that you’ve received the message by setting the shared property to null or some other value. You can respond by setting it to a response value.

Since this method doesn’t rely on Flash’s once-per-frame events system, you can perform as many set or get operations as you want every frame. You can perform them in a tight loop (as in the following code) or perhaps just periodically. For example, your worker thread might check only once it’s exhausted the work it has already been told to do. As you’ll see, the checks are much cheaper than messages sent via MessageChannel, so you don’t need to be exceptionally frugal with them.

Now to test the performance of MessageChannel versus this new setSharedProperty-based method. The following tiny example app simply sends 1000 messages back and forth between two threads. The message is always a single-character string, so there’s not much in the way of data copying going on. This allows us to purely measure the overhead of the message-passing system. For fairness, I’ve set the stage’s frame rate to 1000 and done nothing to impede it from reaching that maximum value. In reality, MessageChannel will likely run even slower depending on your actual frame rate.

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;
 
	/**
	* Test to show the speed of passing messages between workers
	* @author Jackson Dunstan (JacksonDunstan.com)
	*/
	public class MessagePassingTest extends Sprite
	{
		/** Output logger */
		private var logger:TextField = new TextField();
 
		/**
		* Log a CSV row
		* @param cols Columns of the row
		*/
		private function row(...cols): void
		{
			logger.appendText(cols.join(",")+"\n");
		}
 
		/** Message channel from the main thread to the worker thread */
		private var mainToWorker:MessageChannel;
 
		/** Message channel from the worker thread to the main thread */
		private var workerToMain:MessageChannel;
 
		/** The worker thread (main thread only) */
		private var worker:Worker;
 
		/** Number of messages to send back and forth in the test */
		private var REPS:int = 1000;
 
		/** Time before the message passing test started */
		private var beforeTime:int;
 
		/** Current message index */
		private var cur:int;
 
		/**
		* Start the app in main thread or worker thread mode
		*/
		public function MessagePassingTest()
		{
			// Setup the logger
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			// If this is the main SWF, start the main thread
			if (Worker.current.isPrimordial)
			{
				startMainThread();
			}
			// If this is the worker thread SWF, start the worker thread
			else
			{
				startWorkerThread();
			}
		}
 
		/**
		* Start the main thread
		*/
		private function startMainThread(): void
		{
			// Try to get the best framerate possible
			stage.frameRate = 1000;
 
			// Create the worker from our own SWF bytes
			worker = WorkerDomain.current.createWorker(this.loaderInfo.bytes);
 
			// Create a message channel to send to the worker thread
			mainToWorker = Worker.current.createMessageChannel(worker);
			worker.setSharedProperty("mainToWorker", mainToWorker);
 
			// Create a message channel to receive from the worker thread
			workerToMain = worker.createMessageChannel(Worker.current);
			workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMainDirect);
			worker.setSharedProperty("workerToMain", workerToMain);
 
			// Start the worker
			worker.start();
 
			// Begin the test where we use MessageChannel to send messages
			// between the threads by sending the first message
			beforeTime = getTimer();
			mainToWorker.send("1");
		}
 
		/**
		* Start the worker thread
		*/
		private function startWorkerThread(): void
		{
			// Get the message channels the main thread set up for communication
			// between the threads
			mainToWorker = Worker.current.getSharedProperty("mainToWorker");
			workerToMain = Worker.current.getSharedProperty("workerToMain");
			mainToWorker.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorker);
		}
 
		/**
		* Callback for when a message has been received from the main thread to
		* the worker thread on a MessageChannel
		* @param ev CHANNEL_MESSAGE event
		*/
		private function onMainToWorker(ev:Event): void
		{
			// Record the message and send a response
			cur++;
			workerToMain.send("1");
 
			// If this was the last message, prepare for the next test where the
			// two threads communicate with shared properties
			if (cur == REPS)
			{
				// We receive "1" on our own Thread (the worker thread) and send
				// "2" in response
				setSharedPropertyTest(Worker.current, "1", "2", false);
			}
		}
 
		/**
		* Callback for when the worker thread sends a message to the main thread
		* via a MessageChannel
		* @param ev CHANNEL_MESSAGE event
		*/
		private function onWorkerToMainDirect(ev:Event): void
		{
			// Record the message and show progress (this version is slow)
			cur++;
			logger.text = "MessageChannel: " + cur + " / " + REPS;
 
			// If this wasn't the last message, send another message to the
			// worker thread
			if (cur < REPS)
			{
				mainToWorker.send("1");
				return;
			}
 
			// The MessageChannel test is done. Record the time it took.
			var afterTime:int = getTimer();
			var messageChannelTime:int = afterTime - beforeTime;
 
			// Run the setSharedProperty test where the two threads communicate
			// by directly setting shared properties on the worker thread. The
			// main thread receives "2", sends "1", and is responsible for
			// starting the process with an initial "1" message.
			beforeTime = getTimer();
			setSharedPropertyTest(worker, "2", "1", true);
			afterTime = getTimer();
			var setSharedPropertyTime:int = afterTime - beforeTime;
 
			// Clear the logger and show the results instead
			logger.text = "";
			row("Type", "Time", "Messages/sec");
			var messagesPerSecond:Number = messageChannelTime/Number(REPS);
			row("MessageChannel", messageChannelTime, messagesPerSecond);
			messagesPerSecond = setSharedPropertyTime/Number(REPS);
			row("setSharedProperty", setSharedPropertyTime, messagesPerSecond);
		}
 
		/**
		* Perform the setSharedProperty test where the two threads communicate
		* by directly setting shared properties on the worker thread
		* @param worker Worker the shared properties are set on
		* @param inMessage Expected message this thread receives from the other
		* @param outMessage Message to send to the other thread
		* @param sendInitial If an initial message should be sent
		*/
		private function setSharedPropertyTest(
			worker:Worker,
			inMsg:String,
			outMsg:String,
			sendInitial:Boolean
		): void
		{
			// Reset the count from the first test
			cur = 0;
 
			// Optionally send an initial outgoing message to start the process
			if (sendInitial)
			{
				worker.setSharedProperty("message", outMsg);
			}
 
			// Send messages until we've hit the limit
			while (cur < REPS)
			{
				// Check to see if the shared property is the incoming message
				if (worker.getSharedProperty("message") == inMsg)
				{
					// Record the message and send a response by setting the
					// shared property to the outgoing message
					cur++;
					worker.setSharedProperty("message", outMsg);
				}
			}
		}
	}
}

Run the test

I ran this test in the following environment:

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

And here are the results I got: (note: all messages/sec values in thousands)

Type Time Messages/sec
MessageChannel 153 6.53
setSharedProperty 62 16.13

Message Passing Performance Chart

The results show the setSharedProperty approach at 2.5x faster than the MessageChannel approach. Clearly, you’ll want to avoid MessageChannel for any high-frequency communication between threads as a major speedup is not that hard to realize by simply using the dropbox-style approach with setSharedProperty.

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