The Flash API has a gem of a class in Proxy. You can use it to customize the behavior of the dot (.), index ([]), delete, and in operators as well as the for-in and for-each-in loops. Today’s article answers a recent comment by exploring the performance implications of all this fancy customizing that Proxy allows.

To use Proxy, you must make a dynamic class that extends Proxy and overrides one or more of Proxy’s methods to customize the behavior of the various operations listed above. Let’s start by looking at the WithProxy class that simply wraps a Dictionary using Proxy:

package
{
	import flash.utils.Proxy;
	import flash.utils.flash_proxy;
	import flash.utils.Dictionary;
 
	public dynamic class WithProxy extends Proxy
	{
		private var __dict:Dictionary;
 
		private var __propNames:Vector.<String>;
		private var __numProps:int;
 
		public function WithProxy()
		{
			__dict = new Dictionary();
			__propNames = new Vector.<String>();
		}
 
		override flash_proxy function callProperty(methodName:*, ...args): *
		{
			var k:*;
			var dict:Dictionary = __dict;
 
			if (methodName == "countKeys")
			{
				var count:int;
				for (k in dict)
				{
					count++;
				}
				return count;
			}
			else if (methodName == "nullValues")
			{
				for (k in dict)
				{
					dict[k] = null;
				}
			}
			return undefined;
		}
 
		override flash_proxy function deleteProperty(name:*): Boolean
		{
			var ret:Boolean = (name in __dict);
			delete __dict[name];
			return ret;
		}
 
		override flash_proxy function getDescendants(name:*): *
		{
			return __dict[name];
		}
 
		override flash_proxy function getProperty(name:*): *
		{
			return __dict[name];
		}
 
		override flash_proxy function hasProperty(name:*): Boolean
		{
			return name in __dict;
		}
 
		override flash_proxy function isAttribute(name:*): Boolean
		{
			return name in __dict;
		}
 
		override flash_proxy function nextName(index:int): String
		{
			return __propNames[index-1];
		}
 
		override flash_proxy function nextNameIndex(index:int): int
		{
			if (index == 0)
			{
				var propNames:Vector.<String> = __propNames;
				propNames.length = 0;
				var size:int;
				for (var k:* in __dict)
				{
					propNames[size++] = k;
				}
				__numProps = size;
			}
 
			return (index < __numProps) ? (index+1) : 0;
		}
 
		override flash_proxy function nextValue(index:int): *
		{
			return __dict[__propNames[index-1]];
		}
 
		override flash_proxy function setProperty(name:*, value:*): void
		{
			__dict[name] = value;
		}
	}
}

And now a version that doesn’t use Proxy- the WithoutProxy class:

package
{
	import flash.utils.Dictionary;
 
	public class WithoutProxy
	{
		private var __dict:Dictionary;
 
		public function WithoutProxy()
		{
			__dict = new Dictionary();
		}
 
		public function countKeys(): int
		{
			var count:int;
			for (var k:* in __dict)
			{
				count++;
			}
			return count;
		}
 
		public function nullValues(): void
		{
			var dict:Dictionary = __dict;
			for (var k:* in dict)
			{
				dict[k] = null;
			}
		}
 
		public function deleteProperty(name:*): Boolean
		{
			var ret:Boolean = (name in __dict);
			delete __dict[name];
			return ret;
		}
 
		public function getDescendants(name:*): *
		{
			return __dict[name];
		}
 
		public function getProperty(name:*): *
		{
			return __dict[name];
		}
 
		public function hasProperty(name:*): Boolean
		{
			return name in __dict;
		}
 
		public function isAttribute(name:*): Boolean
		{
			return name in __dict;
		}
 
		public function iterate(): Dictionary
		{
			var props:Dictionary = new Dictionary();
			var dict:Dictionary = __dict;
			for (var k:* in dict)
			{
				props[k] = dict[k];
			}
			return props;
		}
 
		public function setProperty(name:*, value:*): void
		{
			__dict[name] = value;
		}
	}
}

To see the usage of these classes and to check their correctness, here’s a small app that does the same actions with both of them and displays the results side by side:

package
{
	import flash.display.*;
	import flash.utils.*;
	import flash.text.*;
 
	public class ProxyTest extends Sprite
	{
		private var __logger:TextField = new TextField();
		private function log(msg:*): void { __logger.appendText(msg + "\n"); }
 
		public function ProxyTest()
		{
			__logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(__logger);
 
			testWithProxy();
 
			var loggerWidth:Number = __logger.width;
			__logger = new TextField();
			__logger.x = loggerWidth + 5;
			__logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(__logger);
 
			testWithoutProxy();
		}
 
		private function testWithProxy(): void
		{
			log("----- WithProxy -----");
 
			var proxy:WithProxy = new WithProxy();
 
			log('proxy["Jackson"] = "Dunstan"');
			proxy["Jackson"] = "Dunstan";
 
			log('"Jackson" in proxy: ' + ("Jackson" in proxy));
			log('proxy["Jackson"]: ' + proxy["Jackson"]);
 
			log("proxy.countKeys(): " + proxy.countKeys());
 
			log("proxy.nullValues(): " + proxy.nullValues());
 
			log('"Jackson" in proxy: ' + ("Jackson" in proxy));
			log('proxy["Jackson"]: ' + proxy["Jackson"]);
 
			log('delete proxy["Jackson"]');
			delete proxy["Jackson"];
 
			log('"Jackson" in proxy: ' + ("Jackson" in proxy));
 
			log('proxy["Jackson"] = "Programmer"');
			proxy["Jackson"] = "Programmer";
 
			log("proxy..Jackson: " + proxy..Jackson);
 
			log('proxy["Circle"] = "Round"');
			proxy["Circle"] = "Round";
 
			log("for-in...");
			for (var k:* in proxy)
			{
				log("    " + k + " -> " + proxy[k]);
			}
 
			log("for-each-in...");
			for each (var adjective:String in proxy)
			{
				log("    " + adjective);
			}
		}
 
		private function testWithoutProxy(): void
		{
			log("----- WithoutProxy -----");
 
			var proxy:WithoutProxy = new WithoutProxy();
 
			log('proxy.setProperty("Jackson", "Dunstan")');
			proxy.setProperty("Jackson", "Dunstan");
 
			log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson"));
			log('proxy.getProperty("Jackson"): ' + proxy.getProperty("Jackson"));
 
			log("proxy.countKeys(): " + proxy.countKeys());
 
			log("proxy.nullValues(): " + proxy.nullValues());
 
			log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson"));
			log('proxy.getProperty("Jackson"): ' + proxy.getProperty("Jackson"));
 
			log('proxy.deleteProperty("Jackson")');
			proxy.deleteProperty("Jackson");
 
			log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson"));
 
			log('proxy.setProperty("Jackson", "Programmer")');
			proxy.setProperty("Jackson", "Programmer");
 
			log('proxy.getDescendants("Jackson"): ' + proxy.getDescendants("Jackson"));
 
			log('proxy.setProperty("Circle", "Round")');
			proxy.setProperty("Circle", "Round");
 
			log("for-in...");
			for (var k:* in proxy.iterate())
			{
				log("    " + k + " -> " + proxy.getProperty(k));
			}
 
			log("for-each-in...");
			for each (var adjective:String in proxy.iterate())
			{
				log("    " + adjective);
			}
		}
	}
}

The results look like this:

----- WithProxy -----
proxy["Jackson"] = "Dunstan"
"Jackson" in proxy: true
proxy["Jackson"]: Dunstan
proxy.countKeys(): 1
proxy.nullValues(): undefined
"Jackson" in proxy: true
proxy["Jackson"]: null
delete proxy["Jackson"]
"Jackson" in proxy: false
proxy["Jackson"] = "Programmer"
proxy..Jackson: Programmer
proxy["Circle"] = "Round"
for-in...
    Jackson -> Programmer
    Circle -> Round
for-each-in...
    Programmer
    Round
 
----- WithoutProxy -----
proxy.setProperty("Jackson", "Dunstan")
proxy.hasProperty("Jackson"): true
proxy.getProperty("Jackson"): Dunstan
proxy.countKeys(): 1
proxy.nullValues(): undefined
proxy.hasProperty("Jackson"): true
proxy.getProperty("Jackson"): null
proxy.deleteProperty("Jackson")
proxy.hasProperty("Jackson"): false
proxy.setProperty("Jackson", "Programmer")
proxy.getDescendants("Jackson"): Programmer
proxy.setProperty("Circle", "Round")
for-in...
    Circle -> Round
    Jackson -> Programmer
for-each-in...
    Round
    Programmer

Finally, let’s do a performance test to compare the classes:

package
{
	import flash.display.*;
	import flash.utils.*;
	import flash.text.*;
 
	public class ProxyPerformance extends Sprite
	{
		private var __logger:TextField = new TextField();
		private function row(...cols): void
		{
			__logger.appendText(cols.join(",") + "\n");
		}
 
		public function ProxyPerformance()
		{
			__logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(__logger);
 
			var beforeTime:int;
			var afterTime:int;
			var wTime:int;
			var woTime:int;
			var key:String;
			var val:String;
			var REPS:int = 100000;
			var SIZE:int = 100;
			var DELETE_KEY:String = "--keynotfound--";
			var GET_KEY:String = "key0";
			var SET_VAL:String = "val0";
			var i:int;
 
			var wProxy:WithProxy = new WithProxy();
			var woProxy:WithoutProxy = new WithoutProxy();
			for (i = 0; i < SIZE; ++i)
			{
				key = "key" + i;
				val = "val" + i;
				wProxy[key] = val;
				woProxy.setProperty(key, val);
			}
 
			row("Function", "WithProxy", "WithoutProxy");
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				wProxy.countKeys();
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.countKeys();
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("callProperty", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				delete wProxy[DELETE_KEY];
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.deleteProperty(DELETE_KEY);
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("deleteProperty", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				wProxy..key0;
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.getDescendants(GET_KEY);
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("getDescendants", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				wProxy[GET_KEY];
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.getProperty(GET_KEY);
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("getProperty", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				GET_KEY in wProxy;
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.hasProperty(GET_KEY);
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("hasProperty", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				wProxy[GET_KEY] = SET_VAL;
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				woProxy.setProperty(GET_KEY, SET_VAL);
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("setProperty", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				for each (key in wProxy)
				{
					val = key;
				}
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				for each (key in woProxy.iterate())
				{
					val = key;
				}
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("for-each", wTime, woTime);
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				for each (val in wProxy)
				{
					key = val;
				}
			}
			afterTime = getTimer();
			wTime = afterTime - beforeTime;
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				for each (val in woProxy.iterate())
				{
					key = val;
				}
			}
			afterTime = getTimer();
			woTime = afterTime - beforeTime;
			row("for-each-in", wTime, woTime);
		}
	}
}

I ran the performance test with the following environment:

  • 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.7
  • 2.4 Ghz Intel Core i5
  • Mac OS X 10.7.1

And got these results:

Function WithProxy WithoutProxy
callProperty 812 746
deleteProperty 88 29
getDescendants 32 10
getProperty 40 10
hasProperty 22 8
setProperty 43 12
for-each 3135 5367
for-each-in 3144 5383

Proxy Performance Graph

Let’s cut out the loops, which make it hard to see the quicker functions:

Proxy Performance Graph (subset)

As you can see, the performance of WithProxy is usually much worse than WithoutProxy. To summarize:

  • callProperty is only marginally slower, probably due to the if-else chain required
  • Everything else is about 3-4x slower (except loops)
  • Exception: for-in and for-each-in performance is about twice as fast. However, this may be due to my implementation and possible to overcome using an alternate technique. Certainly, creating a whole copy of the Dictionary (as in both cases) is not optimal.

So, you should not be using Proxy in performance-critical code. In addition to forcing you into a slow dynamic class, it also mandates an API that often lacks types and shoehorns you into some slow patterns like an if-else chain for callProperty. The Proxy class does work though: it’s behavior is correct and it does allow you to override the default behavior of several AS3 operators. Just don’t use it where you need high performance.

There’s a lot of code above. Have you spotted a bug? Have a suggestion to optimize either version? Did I miss something about Proxy? By all means, post a comment about it!