Tutorials, extensions, and source files for ActionScript, Flash, and other Adobe products.

 

Pages: 1 | 2

Asynchronous ActionScript Execution

Date September 19, 2009
Language ActionScript 3.0
Target Flash Player 9+

Introduction

In Flash Player, both the execution of ActionScript and screen rendering is handled in a single thread. In order for the screen to be rendered, all code execution must be completed. For a Flash Player SWF running at 24 frames per second, this allows all ActionScript operations run within a single frame (frame scripts, events, etc.), at most, around 42 milliseconds to complete - this not accounting for the amount of time necessary to perform the actual screen rendering which itself may vary. If the execution of a block of code requires more time, Flash Player will appear to lock up until it is finished, or, after a default timeout period, just stop executing code.

Timeout error
Script timeout error dialog (Debugger Player only)

This tutorial will cover solutions to this problem - when you have operations that contain ActionScript calculations requiring more time than that which is allotted within any given frame. Such calculations are needed to be executed asynchronously; they would be non-blocking and complete at some point in time after the code block which invoked them has completed its own execution. This would give the renderer a chance to draw one or more times before the calculation is complete preventing the player from locking up.

Requirements

  • Source files (Flash CS4 format; ActionScript externalized)

Table of Contents

Page 1

Page 2

Concepts

A running Flash Player instance processes a constant loop of consecutive frames. Even if there is not necessarily a timeline to animate through, frames are still rendered in the sense that a single frame representing the SWFs contents is repeatedly processed. Each frame consists of what can broken down into 2 parts: the execution of ActionScript through the AVM or ActionScript Virtual Machine, and a visual rendering of the screen by the Flash Player renderer. Code execution consists of both running ActionScript as well as idle time leading up to the next render. ActionScript is arbitrarily executed based on actions and events that occur between screen renders so idle time and occurrences may vary.

ActionScript frame
Frames both execute ActionScript and render the screen

When idle time is eaten away by the execution of cpu-intensive ActionScript, the time between each frame render will take longer, and the playback frame rate will suffer. What was 1 frame executed in 42 milliseconds may instead take 62 milliseconds, or even more if the ActionScript requires it.

:ong ActionScript
ActionScript taking a long time to complete delays rendering

If ActionScript could be processed in a different thread of execution, this could prevent the renderer from being blocked. This is, however, not possible, as Flash Player, by nature, uses only one thread for both code execution and the rendering of the screen. So to prevent processor-intensive ActionScript from blocking the renderer, its calculations must be broken up into smaller, individual segments, or chunks, that can be run individually across multiple frames. Between each segment, the renderer can redraw allowing playback to go uninterrupted.

Segmented ActionScript
ActionScript stretched across multiple frames

This division of a single block of executing code across multiple frames is often non-trivial. Such code needs not only to know when to stop, but how to get back to where it was when picking up from where it left off. This adds the dependency of persistent data. What was before a function block with local variables must now become an object with property values.

Single-dimension Loops

The simplest, and probably the most common, circumstance requiring code to be segmented into chunks is with looping. A long loop can easily take up a lot of processing time, especially if heavy calculations are being made within each iteration of that loop.

Standard for loops use an index variable to keep track of a location within a list of items to be handled during iteration. A standard array loop would look something like:

// pseudo code
var i, n = array.length;
for (i=0; i<n; i++){
	process(array[i]);
}

Segmenting the loop so that it can be handled in multiple iterations would require breaking out of the loop in the middle of the iteration with the ability to return to it later starting with the value of the index i at the point in time when the break occurred. The retained value of i represents the persistent variable that needs to be saved so that it can be referred to in another frame.

// pseudo code
var savedIndex = 0;
var i, n = array.length;
for (i=savedIndex; i<n; i++){

	if (needToExit()){
		savedIndex = i;
		break;
	}

	process(array[i]);
}

Since the point of segmenting is to allow frame rendering, the execution of each chunk is appropriately handled in an Event.ENTER_FRAME event. An event handler is set when the loop starts and would need to be removed when the loop is complete. The loop is complete when the loop finishes without being exited, which, in the conext of a function would be handled by return.

// pseudo code
var savedIndex = 0;
function enterFrame() {

	var i, n = array.length;
	for (i=savedIndex; i<n; i++){

		if (needToExit()){
			savedIndex = i;
			return;
		}

		process(array[i]);
	}

	complete();
}

The determination for loop exiting is decided by needToExit()whose implementation is ultimately up to you. This can be based on a set number of iterations, or even based off of an elapsed period of time as determined by getTimer(). Ideally, the time allotted for execution would be just enough to fill all available idle time up until the next render. There is no way to know how much time that actually is, so approximations will no doubt be a part of this process.

Example: Caesar Cipher

This example puts to practical application the looping of the characters within a string divided over multiple frames. To keep things self-contained and organized, a class, CaesarCipher, will be used to retain the necessary persistent data for the loop, though this data could also be retained within the current working scope (such as in timeline variables in Flash authoring).

The CaesarCipher class applies a Caesar cipher to a string. A standard, synchronous function for this operation would appear as:

function caesarCipher(text:String, shift:int = 3):String {
	
	var result:String = "";
	var i:int;
	var n:int = text.length;
	
	for (i=0; i<n; i++){
		
		var code:int = text.charCodeAt(i);

		// shift capital letters
		if (code >= 65 && code <= 90){
			code = 65 + (code - 65 + shift) % 26;
		}
		
		// shift lowercase letters
		if (code >= 97 && code <= 122){
			code = 97 + (code - 97 + shift) % 26;
		}
		
		result += String.fromCharCode(code);
	}
	
	return result;
}

The CaesarCipher class uses this same logic but allows the operation to continue over multiple frames before it's considered complete. This process then becomes asynchronous. Certain things had to be considered when implementing this:

  • Only display objects receive Event.ENTER_FRAME events
  • Return values no longer apply to asynchronous operations

The goal of the CaesarCipher class is to processes data; instances of it are not meant to be displayed on the screen. As a result, it will not be based off of another DisplayObject class which keeps the Event.ENTER_FRAME event from being available. This dependency, however, can be easily satisfied with composition, including a simple DisplayObject class member such as a Shape instance within the CaesarCipher class definition. Even though that instance will never reach a display list, it will still dispatch the Event.ENTER_FRAME event that can be used by an instance of CaesarCipher.

Concerning return values, an approach used by existing implementations of asynchronous operations will need to be used. For example, consider the URLLoader class. Instances are used to load external content through an asynchronous operation started by the load() call. When complete, an Event.COMPLETE event is dispatched and the loaded data can be accessible through the data property of the URLLoader instance. The CaesarCipher class uses a similar implementation. To start the operation, a run() method is used. When complete, an Event.COMPLETE event will fire, and the would-be return value can be accessible through the result property.

Usage:

var text:String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var cipher:CaesarCipher = new CaesarCipher(text, 3);
cipher.addEventListener(Event.COMPLETE, complete);
cipher.run();

function complete(event:Event):void {
	trace("Result: " + cipher.result);
	// Result: Oruhp lsvxp groru vlw dphw, frqvhfwhwxu dglslvflqj holw.
}

View live CaesarCipher example.

Though this class retains its result data in a class member variable, it could also be passed passed to the event, for example, if using a TextEvent object for the complete event. This approach, however, would necessitate the creating of a new Event class to handle the results data, so it's usually easier to stick to a class variable.

Because this example is using a short string, and because a Caesar cipher is not especially difficult for ActionScript to pull off, an iteration counter was used to determine when the next frame should be allowed to render between processing steps. It may be more common that a time-based approach will be used to make sure you're getting the most out of what time you have to calculate results in your frame.

Time-based Exit Conditions

Exiting an asynchronous script based off of the time taken to process a calculation will likely be the best solution for handling the segmentation. For this, you would need to know how much time has passed since the current frame's calculations started, and how much time, per frame, the calculations are allowed to have.

//pseudo code
var allowedTime = 1000/fps - renderTime - otherScriptsTime;
var startTime = 0;
var savedIndex = 0;
function enterFrame() {
	startTime = getTimer();
	
	var i, n = array.length;
	for (i=savedIndex; i<n; i++){

		if (getTimer() - startTime > allowedTime){
			savedIndex = i;
			return;
		}

		process(array[i]);
	}

	complete();
}

The times for rendering and other scripts will be estimated, but the more accurate they are, the better use of the available frame time you'll get. Note that these values can change depending on processor speed of computer running the script which can make them variable even at runtime. The closer it is to filling up the time available in the frame the better. You may even want to overshoot a little bit to make processing go as fast as possible at the expense of the frame rate depending on what's being presented to the user when the operation is being run.

For operations that contain a lot of looping with smaller calculations, you may not want the exit condition to be checked in every loop iteration - this especially when using getTimer() since it will help reduce the overhead of calling the function and looking up that value. The fewer checks made, the faster processing will go. It can be a tricky balancing act between when to do what and how many times to do it.

Most examples covered here will not use time for exit conditions simply for means of demonstration, though they may be better suited to use it, especially for larger scale implementations.

Multi-dimension Loops

Data represented in multiple dimensions, such as grid data, or the pixels in an image, often necessitate nested loops. When converting these loops into asynchronous operations, a similar approach to the asynchronous single loops is used. With nested loops, however, extra attention is needed to facilitate the initialization of loop variables when resuming a the loop. Specifically, it becomes important that saved indices for nested loops be reset to 0 at the end of the loop for its next iteration.

// pseudo code
for (i=savedIndexI; i<n; j++){

	for (j=savedIndexJ; j<m; j++){

		process(array[i][j]);
	}
		
	savedIndexJ = 0;
}

The resetting of the initial index value will have to be done for each nested loop and only after the first loop has already run. This is necessary to make sure the saved index is not reset before its able to be used for the first iteration of that loop.

Example: Color Gradient

Processing pixels in a bitmap is a common case where a nested loop would be needed. This example, using a class named RenderGradient, draws a gradient into a bitmap pixel by pixel using the setPixel() method of the BitmapData class.

The behavior of the RenderGradient class very closely follows that of the CaesarCipher class in the previous example. The class structure is almost identical with only a few notable exceptions:

  • Two loops are used; the inner loop index variable (savedX) is reset at the end of the loop block
  • A separate counter variable, iterationCounter, is used to count loop iterations for segmentation
  • Validation of the referenced BitmapData is required each time the operation is resumed
  • No return variable is necessary - the operation edits existing content rather than generating its own

The validation step is one worth noting. This was not an issue with the Caesar cipher example because all utilized data was copied into class member variables of the CaesarCipher instance. For RenderGradient, a BitmapData object is stored as a reference (through the bmp variable); it is not copied. The original BitmapData is modified as calculations are being made. This is important because if at some point the referenced BitmapData instance is disposed of by another process (using dispose()), the calculations made by the RenderGradient class would fail. This not unlike concurrency problems encountered in multi-threaded environments. In fact, these asynchronous operations are very thread-like and with references to external objects can experience similar consequences of thread interference. With ActionScript, however, you are at least guaranteed that no two scripts are being executed at the same time. This allows a one-time validation step at the start of an operation to handle such inconsistencies or unexpected states in referenced data. RenderGradient does this in a try..catch when defining the xn and yn variables from the width and height of the referenced BitmapData in the loop handler. Disposed BitmapData objects will cause an error when getting the width and height values allowing RenderGradient to exit gracefully with an Event.CANCEL event should an error occur there.

RenderGradient's validation does not prevent multiple instances from acting upon the same bitmap. It would be quite possible to have multiple RenderGradient instances editing the pixels of the same bitmap. The last instance called would simply overwrite all of the changes made by the previous. Other situations may require different levels of validation.

Usage:

var renderer:RenderGradient = new RenderGradient(bmp, 0xFF0000, 0x0000FF);
renderer.run();

View live RenderGradient example.

Since all changes occur during the operation and no result value is needed, no complete event handler has been included here, though it may be necessary if there are other dependencies waiting for its completion.

Handling for..in and for..each Loops

Each of the previous examples use indexed loops for pausing and resuming iteration across multiple frames. They do not consider non-indexed looping such as for..in and for..each loops. These loops lack the ability to restart at an arbitrary location within the iteration. This prevents them from working very well with segmentation used to divide loops into smaller parts.

To handle these loops, you would need to first convert them into an indexed list, then use that list with segmentation.

// pseudo code
var indexedList = [];
for each(value in object){
	indexedList.push(value);
}

// proceed normally...

This does incur a little extra overhead at the beginning of the operation but assuming the for/for..each loop itself isn't the cause of the slow processing, it should be negligible.

Continue reading »

Pages: 1 | 2