Send ActionScript Worker Messages 2.5x Faster
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); } } } } }
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 |
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!
#1 by henke37 on October 28th, 2013 ·
How does this compare to using shared memory and Condition/Mutex?
#2 by jackson on October 28th, 2013 ·
Sounds like some great ideas for followup articles. :)
#3 by Ben on October 28th, 2013 ·
I got strange results on my system:
Release version of Flash Player 11.9.900.117
3.4 Ghz Intel Core i7 (32 GB RAM)
Windows 7 64
MessageChannel,80,0.08
setSharedProperty,144,0.144
Sometimes the MessageChannel method is faster and sometimes the setSharedProperty. I tried it in several browsers (IE, Opera and FF) and it is always the same: if I reload the page the values have a random range from 40 to 160 on both methods.
#4 by jackson on October 28th, 2013 ·
This may be due to the speed of
MessageChannel
being linked to your frame rate. Could you try out this version that uses 30 frames-per-second and see if you get different results?#5 by Ben on October 29th, 2013 ·
With the new version the results are much more consistent. Sometimes I got an outlier and setSharedProperty is slower but in 95% of the runs it seems to be faster.
Type,Time,Messages/sec
MessageChannel,73,0.073
setSharedProperty,133,0.133
#6 by Alan G. on October 28th, 2013 ·
First, thanks for the solution; I enjoy reading your contributions to the AVM community.
I think something went wrong with the messages per second calculation:
MessageChannel 153 0.153/sec
setSharedProperty 62 0.062/sec
If the times per message are in milliseconds (1000 x time per message), then the table should look a bit different:
MessageChannel 153 6.53/sec
setSharedProperty 62 16.13/sec
or are we talking about *seconds per message* instead of “Messages/sec”? (1 / time per message) In that case its still
MessageChannel 153 6.53/sec
setSharedProperty 62 16.13/sec
That’s still very slow – we would be talking about hundreds of thousands of messages per second *or faster* if this was native code. Example: http://zeromq.org/whitepapers:y-suite. What is holding the message rate back so much? Is there synchronization per frame between workers ?
#7 by jackson on October 28th, 2013 ·
Thanks for pointing this out. I must have accidentally posted from an earlier version of the code that had a buggy “messages per second” calculation. I’ve updated the results table and graphs in the article.
As to why this is so slow, I’m not sure. AS3/AVM typically have huge performance penalties over native code, but this does seem abnormally large. It’s very difficult to say what’s actually occurring when you call
setSharedProperty
and the like since all the code is closed source and, if you’ve ever programmed multi-threaded code in native, clearly a huge abstraction over what’s going on “behind the scenes”. How issetSharedProperty
implemented? How isMessageChannel
implemented? I suppose that’s a question for Adobe.#8 by benjamin guihaire on October 28th, 2013 ·
MessageChannel,117,0.117
setSharedProperty,49,0.049
MacBook Pro, 2.3Ghz Intel Core I7
#9 by Glidias on November 3rd, 2013 ·
Instead of a single string, can the test be done with multiple numbers? I’m not sure how this compares compared to a sharable Bytearray for such a case. It’s quite complicated to manually manage polling and synchronizing the 2 values (especially with multiple watcher pairs, not just 1 pair), compared to a traditional event/notification system (which will simply instantiate new event objects, albeit with a cost) back and forth. Of course, you could do a watcher-based utility for such a thing…but i wonder how well it’ll scale with how many watcher pairs and how many varying frequencies.
#10 by jackson on November 4th, 2013 ·
Check out my reply to a similar comment of yours on the previous article. Especially pertinent is the next article which uses
Mutex
to implement a “watcher” system that might suit your needs.#11 by Tim on December 26th, 2013 ·
Offhand, mentioned in this comment: “(because i’m lazy to create multiple message channels even though I should…).”
Is it usually better to create multiple message channels between two workers for specific tasks? I was playing with something and was having issues that looked like messages were conflicting and piling up. One of the many changes I made was separating out a shared message channel in to a couple of message channels for each type of message. This seemed to help significantly, but I’m still wondering/digging if this change was the key or if it was something else I did.
Reading above, it mentioned a MessageChannel gets its message just once per frame; would this just be for a single channel or multiple channels? ie: 1 MessageChannel limited to 1 message per frame, but 4 message channels can get 4 messages per frame?
#12 by David on February 3rd, 2016 ·
Super analyse, thanks.
What about using socket instead MessageChannel? I did not try, will do if no one did it.
I hope that communication via sockets could be faster than shared properties or channels.
#13 by jackson on February 4th, 2016 ·
The network/TCP-IP
Socket
class?