Original Site:http://jacksondunstan.com/articles/2416 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.
Original Site:http://jacksondunstan.com/articles/2416
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:
-debug=false -verbose-stacktraces=false -inline -optimize=true
)And here are the results I got:
Type | Time | Messages/sec |
---|---|---|
MessageChannel | 153 | 6.53 |
setSharedProperty |
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
.