Pseudo Threads
Eventually, Flash Player will support Worker Threads to take advantage of multi-core CPUs, but that may be quite a while from now. Today’s article shows you how you can get some concurrency right now by faking threads. Read on for the technique!
As implied above, your AS3 code has to be single-threaded. However, you are free to concurrently run JavaScript in the browser while your AS3 runs. The ExternalInterface class makes it easy to talk between AS3 and JavaScript through simple function calls. So, if you want to have JavaScript do some heavy computations for you (and why not? it’s often faster) while your Flash app continues running, here’s the procedure:
- Set a callback for when JavaScript is done using
ExternalInterface.addCallback
. Let’s call itonResult
. - Use
ExternalInterface.call
to call a JavaScript functionstart
- So that the
ExternalInterface.call
function doesn’t block execution,start
callssetInterval
to do the actual work later in another function:doWork
- When
doWork
is done, it callsonResult
on the Flash HTML object and passes it the result
While the JavaScript “thread” is working, the AS3 app can continue doing whatever it pleases. The following app has AS3 and JavaScript functions to do some heavy work: compute the sum of the first N square roots. Click the buttons to do this work with just Flash, just JavaScript, or both of them using the above pseudo-threading model.
package { import flash.external.*; import flash.display.*; import flash.system.*; import flash.events.*; import flash.utils.*; import flash.text.*; [SWF(width=500,height=400,backgroundColor=0xEEEAD9)] public class PseudoThreads extends Sprite { private var __logger:TextField = new TextField(); private function log(msg:*): void { __logger.appendText(msg + "\n"); } private static const TEXT_FORMAT:TextFormat = new TextFormat("_sans", 11); private static const NUM:int = 100000000; private static const INTERVAL:int = 1; private static const NEEDED_RESULTS:int = 20; private static const PAD:Number = 3; private var __numResults:int; private var __intervalName:uint; private var __beginTime:int; public function PseudoThreads() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; Security.allowDomain("*"); ExternalInterface.addCallback("reportResult", onResult); makeButton("Flash Only", onFlash); makeButton("JavaScript Only", onJS); makeButton("Flash and JavaScript", onBoth); var about:TextField = new TextField(); about.autoSize = TextFieldAutoSize.LEFT; about.defaultTextFormat = TEXT_FORMAT; about.htmlText = '<font color="#0071BB">' + '<a href="http://JacksonDunstan.com/articles/1427">' + 'JacksonDunstan.com' + '</a></font>\n' + 'September 2011'; about.x = stage.stageWidth - PAD - about.width; about.y = PAD; addChild(about); __logger.y = this.height + PAD; __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); } private function makeButton(label:String, callback:Function): void { var tf:TextField = new TextField(); tf.defaultTextFormat = TEXT_FORMAT; tf.name = "label"; tf.text = label; tf.autoSize = TextFieldAutoSize.LEFT; tf.selectable = false; tf.x = tf.y = PAD; var button:Sprite = new Sprite(); button.name = label; button.graphics.beginFill(0xE6E2D1); button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2); button.graphics.endFill(); button.graphics.lineStyle(1, 0x000000); button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2); button.addChild(tf); button.addEventListener(MouseEvent.CLICK, callback); button.x = PAD + this.width; button.y = PAD; addChild(button); } private function onFlash(ev:MouseEvent): void { startFlash(); start(); } private function onJS(ev:MouseEvent): void { startJS(); start(); } private function onBoth(ev:MouseEvent): void { startFlash(); startJS(); start(); } private function start(): void { __logger.text = ""; __beginTime = getTimer(); log("Getting " + NEEDED_RESULTS + " results..."); } private function startFlash(): void { __intervalName = setInterval(onInterval, INTERVAL); } private function startJS(): void { ExternalInterface.call("start", NUM, INTERVAL); } private function onResult(val:Number): void { result("JavaScript", val); } private function onInterval(): void { result("Flash", sumOfSqrts(NUM)); } private function result(from:String, val:Number): void { __numResults++; log("Result #" + __numResults + " from " + from + ": " + val); if (__numResults >= NEEDED_RESULTS) { var time:int = getTimer() - __beginTime; log("Time for " + NEEDED_RESULTS + " results: " + time); ExternalInterface.call("stop"); if (__intervalName) { clearInterval(__intervalName); __intervalName = 0; } __numResults = 0; } } private function sumOfSqrts(n:int): Number { var total:Number = 0; for (var i:int; i < n; ++i) { total += Math.sqrt(i); } return total; } } }
You can see that it is working by the following Activity Monitor screenshots. They show high CPU usage for “Shockwave Flash (Chrome Plug-In Host)” when Flash is working and “Google Chrome Renderer” when JavaScript is working.
Flash Only
JavaScript Only
Flash and JavaScript
Obviously, the performance you see will vary from browser to browser and depend heavily on your CPU’s features, such as number of cores, hyperthreading, etc. Here is the test environment I used:
- Flex SDK (MXMLC) 4.5.1.21328, compiling in release mode (no debugging or verbose stack traces)
- Release version of Flash Player 10.3.183.10
- Google Chrome 14.0.835.186
- 2.4 Ghz Intel Core i5
- Mac OS X 10.7.1
I got these results:
Type | Time |
---|---|
Flash Only | 41285 |
JavaScript Only | 26664 |
Flash and JavaScript | 25285 |
Granted, the speedup is not even close to double despite my dual-core setup. Actually, I got about a 7% boost compared to pure JavaScript and a 40% boost when compared with pure AS3. However, there may be tasks that are more suited to this than the fake number crunching I set up. Further, it may work out better on quad-core machines or other operating systems or browsers. That said, it may also work out worse.
Any suggestions you can give on how to improve the “pseudo-threads” model would be appreciated. The above is certainly only a “rough draft” of what’s possible when running Flash and JavaScript at once and it’s likely that I’ve missed something that will make them work together better.
#1 by Ben on September 26th, 2011 ·
My results are different.
CPU: i7-2600 3.40 GHz
8 GB RAM
Windows 7 64 Bit
All browsers are running FP 11.0.1.129.
Opera 11.51 (build 1087 32 Bit):
Flash only: 13870
JavaScript only: 20050
Flash and JavaScript: 14628
Firefox 6.0 32 Bit:
Flash only: 15151
JavaScript only: 32336
Flash and JavaScript: 12212
Internet Explorer 9.0.8112.16421 64 Bit:
Flash only: 16315
JavaScript only: 146338
Flash and JavaScript: 117670
Sometimes it seems that JavaScript slows Flash down. I could only see a lower time in Firefox. JavaScript in the Internet Explorer 64 Bit seems to be extremely slow.
Also this seems to confirm that Flash under Windows is much faster than on Mac OS X.
#2 by jackson on September 26th, 2011 ·
A 20% speedup in Firefox is nothing to sneeze at. :)
Since this is so browser-dependent, you might want to only do it when you detect that the conditionals are optimal, perhaps with a whitelist (e.g. Firefox >= 4 || Chrome >= 12) or a blacklist (e.g. IE || Opera).
#3 by as3isolib on September 26th, 2011 ·
Due to the nature of the rendering engine in the as3isolib.v2, I have toyed with the idea of offloading some of the computational logic to Alchemy or JS or something else. One thing that has always preventing me from investing the time in testing this is the fact that anytime Flash makes a call outside it’s own domain, there is a cost or lag.
Can you speak more on some of the pros/cons to utilizing say a call to and from externalInterface on enterFrame? What would be the best handoff policy for that. Say every frame I wish to hand off to JS and then receive the previous frames’ response back from JS for rendering?
#4 by jackson on September 26th, 2011 ·
There’s definitely a cost to using
ExternalInterface
, but I haven’t measured it. Anything that dynamic just has to be slow. This is why I think it’s a good policy to only offload large chunks so that your AS3<->JavaScript communication is rather infrequent. With that in mind, I wouldn’t think that it’s something you should do every frame in a rendering engine. However, you could do it for some larger tasks such as pre-loading/building the next level to display, pre-building areas of the scene not yet scrolled to, etc.#5 by jonathan on September 26th, 2011 ·
You can use “worker” in javascript, or use another swf to do some computation.
#6 by as3isolib on September 26th, 2011 ·
Unfortunately using another SWF will kill the overall performance gains I am hoping to achieve since you have a set “performance bandwidth” that is utilized by ALL SWFs running concurrently.
I guess the question is: is the per frame callout and callin to ExternalInterface per frame more performant than handling it “in house” by the SWF.
#7 by jackson on September 26th, 2011 ·
That’s really an excellent point: this technique could be adopted such that the AS3 app spawns JavaScript which in turn spawns JavaScript workers to do real concurrency on the JavaScript side. So you’d have 1 Flash thread + N JavaScript threads.
At least on some browsers… :)
#8 by AlexG on September 26th, 2011 ·
private static const NUM:int = 100000000; LOL A hundred million operations :D
It overloaded my browser for a long time. Could you please use smaller amounts?
#9 by jackson on September 26th, 2011 ·
The point was to offload tasks that take a while to complete and a hundred million seemed suited for my machine. The source code is posted if you’d like to drop a zero or two… ;-)
#10 by AlexG on September 26th, 2011 ·
And by the way if you need to perform simple calculations use Pixel Bender. Must be faster than both Flash or JavaScript.
#11 by Trope on September 27th, 2011 ·
Another way to parallelize SWFs is to use LocalConnection and N different browsers. ;) As far as I know two SWFs will not run parallel under same browser.
#12 by jackson on September 27th, 2011 ·
Now that would present an interesting design challenge: how to get the user to fire up an extra browser…
#13 by skyboy on October 4th, 2011 ·
Ask nicely. ;)
I actually have a mostly complete system set up such that N SWFS connected using the LocalConnection protocol I set up can communicate with each other and share data. They all do the same work in this case (it’s a radar for a long forgotten project), but the protocol could be adapted if finished to distribute the work load and task list among all running flash instances; be it a stand-alone projector, in a browser or what have you. It’s unintentionally an implementation of a P2P network like Skype’s, created before Skype actually came out. I’d comment about being copied, but it was never released.
A “main” SWF establishes the first connection and waits for others to join, and defers off a second channel for communication between them and distributes a list of connected clients after dispatching a join event to the other clients; they regularly ping each other, and any that fail to respond are dropped, if the main SWF is dropped, another one establishes itself. It needs failure handling on that part in case an SWF not in the client list manages to establish one first, but it’s mostly fool proof.
#14 by jackson on October 4th, 2011 ·
Sounds cool. Any chance you’ll release it? (e.g. to your Github?)
#15 by skyboy on October 8th, 2011 ·
Sadly, it would require porting to AS3, as the project was AS2, and fixing a few existing bugs and race conditions. Not sure when I’ll have the time to do it or that it’d be that useful; an AIR project could start up multiple SWFs to run like that, but I don’t know that the processing load would be distributed in a way that makes it useful. Regular browser SWFs wouldn’t have that luxury, either.
On top of the limited cases where it can be used, Workers are being implemented for a later FP11.x(?) release, and they will allow more than just Strings to be copied around. Porting and fixing it now would be akin to building a car from a wagon so you can go to the store in 5 days when you can get an actual car in 7 days.
#16 by ben w on September 28th, 2011 ·
I tried something like this a while back to try and palm off some 3d calculations to JS sadly the sending and receiving of the data made it impossible to use as a real time solution :(
If I remember rightly (and I probably don’t) even just getting JS to add a few numbers together and send it back took its sweet time (like over 10 ms)
I considered sockets with a Java applet but couldn’t get that fast enough either.
#17 by Antti H. on October 3rd, 2011 ·
I can confirm that Flash is much faster than Javascript in this benchmark on Windows 7 64bit. It seems that there is a nice speedup with Flash Player 11 rc1 compared to Flash 10.3.
Intel 2600K @ 4.6 Ghz:
FireFox 7.01, Flash 10.3.183.10:
Flash: 15334ms
Javascript: 24347ms
Flash+JS: 12088ms
FireFox 7.0.1, Flash 11.0.129:
Flash: 11823ms
Javascript: 24272ms
Flash+JS: 8836ms
Safari 5.1, Flash 11.0.129:
Flash: 11675ms
Javascript: 22713ms
Flash+JS: 14973ms
Interesting results… fuel for fanboys? ;)
#18 by dimuMurray on October 7th, 2011 ·
You can exploit (to an extent) the power of parallel processing right now in Flash via PixelBender. Check out the following article on Adobe’s Developer Connection:
http://www.adobe.com/devnet/flex/articles/flashbuilder4_pixelbender.html