Monday, December 7, 2009

Asynchronous Computation

Trevor McCauley's article about asynchronous ActionScript execution provides a brilliant way to distribute CPU-consuming computation from one frame to multiple frames. I was so excited when I first read this article, eager to try it out so much, and finally, today I had the chance to give it a try.

One of my classmate was working on a project for a course on social networks. He used ActionScript for his project, trying to parse an XML file representing a graph of over 80 thousand nodes and reconstruct the graph using his own classes. If you, whether intentional or not, had somehow make your Flash program enter an infinite loop, you might know that Flash Player throws a time-out error if a single frame takes more than 15 seconds to render.

His XML file was so big that even parsing it from string to the AS3 internal XML type took a couple of seconds, let alone instantiating his own classes representing nodes and edges for that graph, and as I expected, he was faced with the 15-second-error nightmare.

I applied Trevor's idea to solve this problem by factoring out the instantiation of his classes and distribute it across multiple frames, and things worked out perfectly and beautifully!

Thanks Trevor. Love you :)

Here's some code snippet:


//loader for loading external XML file
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onLoad);

//start loading the XML file
loader.load(new URLRequest("myXML.XML"));

//XML variables
var xml:XML;
var nodes:XMLList;
var edges:XMLList;

//graph model
var graph:Graph = new Graph();

//asynchronous computation variables
var currentIndex:int; //this index keeps track of the progress of asynchronous computations
var length:int;
var startTime:int;
var allcatedTimePerFrame:int = 16; //approximately 60fps

//loader complete event handler
function onLoad(e:Event):void {
xml = XML(loader.data);
nodes = xml.node;
edges = xml.edge;

//initialize asynchronous computation variables
currentIndex = 0;
length = nodes.length();

//start building nodes asynchronously
addEventListener(Event.ENTER_FRAME, buildNodes);
}

//asynchronous computation for building nodes
function buildNodes(e:Event):void {
//take a snapshot of the current time
startTime = getTimer();

for (var i:int = currentIndex; i < length; i++) {
//build nodes in the graph model
graph.createNode(nodes[i].@name);
currentIndex++;

//check if the computation is complete
if (currentIndex == length) {
//stop this listener
removeEventListener(Event.ENTER_FRAME, buildNodes);

//initialize asynchronous computation variables
currentIndex = 0;
length = edges.length();

//start buliding edges
addEventListener(Event.ENTER_FRAME, buildEdges);
return;
}

//check if the time spent on this loop exceeds the allocated time per frame
if (getTimer() - startTime() > allocatedTimePerFrame) {
//display the current progress in a text field
progress_txt.text = "building nodes: " + 100 * (currentIndex / length) + "%";
return;
}
}
}

//asynchronous computation for building edges
function buildEdges(e:Event):void {
//take a snapshot of the current time
startTime = getTimer();

for (var i:int = currentIndex; i < length; i++) {
//build nodes in the graph model
graph.createEdge(edges[i].@node1Name, edges[i].@node2Name);
currentIndex++;

//check if the computation is complete
if (currentIndex == length) {
//stop this listener
removeEventListener(Event.ENTER_FRAME, buildEdges);

//we're done!!
//display the graph on the stage
graph.buildView();
addChild(graph.getView());

return;
}

//check if the time spent on this loop exceeds the allocated time per frame
if (getTimer() - startTime() > allocatedTimePerFrame) {
//display the current progress in a text field
progress_txt.text = "building edges: " + 100 * (currentIndex / length) + "%";
return;
}
}
}

No comments: