Friday, February 03, 2006
How to Speed Up Flash 8's ExternalInterface
I found some interesting new ways of speeding up Flash 8's broken ExternalInterface while working on dojo.flash and dojo.storage that I thought folks might find useful.
I found all of the JavaScript serialization methods in the Flash player (and they are in JavaScript) by using the Microsoft Debugger (they all live on window.parent; you'll see them appear a few seconds after the Flash player loads). These methods handle all of the serialization and deserialization and all begin with the name __flash__. It turns out that they ARE using XML internally, and did not implement a real XML parser on the C++/Flash side which can correctly handle CDATA sections. They are also doing evals(), which is one of the reasons it is slow. As a cool sidenote, you can actually _replace_ them with your own serialization implementations, but only on IE: window.parent.__flash__toXML = function(value) { alert('look at me, I replaced a flash function! I'm cool'); }
Looking at the internal JavaScript code, I found a method named CallFunction on the plugin itself that does all the magic. This function takes XML to invoke a function on the other side:
The other thing I found is that the performance of ExternalInterface does degrade O(n^2), possibly even worse. I plotted a series of data points of its performance for different data sizes, and found that starting at 8K performance goes into the seconds; afterwards it drastically gets worse in an exponential way.
I found a way to get the performance to be linear and acceptable. It turns out if you break the input values and output values into a series of chunks, and push them over each at a time, the performance becomes linear and fine:
I bet they are doing some stupid string handling or something internally that is easy to fix but which causes O(n^2) performance, where n is the data size.
Note that I'm not chopping the results into pieces in the code above, but I experimented with that as well. I found that the optimal size was 1024 a chunk; going larger or smaller degraded performance.
With the chunking above, I could push 1.2 megabytes over in about 2 seconds; if I didn't use the optimizations above, it could easily hang the browser for a minute or more.
The other thing is you have to encode certain values (again because they are using XML and didn't use CDATA sections, so you run into the same double encoding problems that RSS 0.92 used to have):
So, we encode all new line types, which if we don't causes the values to be corrupted; we also encode JavaScript string terminators so that when Flash hands us back its values as JSON, that these terminators don't break the results (Flash also doesn't correctly encode JavaScript string terminators when it hands its results back. Sigh.) We also generically encode any kind of XML/HTML entity reference, rather than hard code specific ones (thats the &$1; thing up there).
In general, whoever coded ExternalInterface should be fired. Great idea, terrible implementation. Have they ever heard of unit tests and boundary conditions?
Anyway, here's the great thing about dojo.flash: you won't have to know about any of the tricks above. It encapsulates you from these things, so that when we don't need them anymore we can swap them out. I had gotten Flash 8 to acceptable performance that degraded nicely using the tricks above, and was all ready to throw out the Flash 6 stuff, when I found out that Flash 8 is not supported on Linux (and they pretty much just got Flash 7 for that platform). Damn.
I found all of the JavaScript serialization methods in the Flash player (and they are in JavaScript) by using the Microsoft Debugger (they all live on window.parent; you'll see them appear a few seconds after the Flash player loads). These methods handle all of the serialization and deserialization and all begin with the name __flash__. It turns out that they ARE using XML internally, and did not implement a real XML parser on the C++/Flash side which can correctly handle CDATA sections. They are also doing evals(), which is one of the reasons it is slow. As a cool sidenote, you can actually _replace_ them with your own serialization implementations, but only on IE: window.parent.__flash__toXML = function(value) { alert('look at me, I replaced a flash function! I'm cool'); }
Looking at the internal JavaScript code, I found a method named CallFunction on the plugin itself that does all the magic. This function takes XML to invoke a function on the other side:
var result = plugin.CallFunction("<invoke name="\"
returntype="\"><arguments><string>" + piece +
"</string></arguments></invoke>");
The results are returned as JSON; the Flash JavaScript does an eval on this. Their eval suffers from the same thing we talked about awhile back, in that they don't detect if they are dealing with a String type to bypass doing the eval(), which can drastically improve performance for large data sets, like XML files, that don't need to be evaled. Using the trick above we can get the direct result without evalling, and intelligently do the eval ourselves.The other thing I found is that the performance of ExternalInterface does degrade O(n^2), possibly even worse. I plotted a series of data points of its performance for different data sizes, and found that starting at 8K performance goes into the seconds; afterwards it drastically gets worse in an exponential way.
I found a way to get the performance to be linear and acceptable. It turns out if you break the input values and output values into a series of chunks, and push them over each at a time, the performance becomes linear and fine:
// cut up the string into pieces
var resultsArray = new Array();
var segments = Math.ceil(testValue.length / 1024);
for(var i = 0; i < segments; i++){
var startCut = i * 1024;
var endCut = i * 1024 + 1023;
if(i == (segments - 1)){
endCut = i * 1024 + testValue.length;
}
var piece = testValue.substring(startCut, endCut);
var result = plugin.CallFunction("<invoke name="\" returntype="\"><arguments><string>" + piece +
"</string></arguments></invoke>");
resultsArray.push(result);
}
var results = resultsArray.join("");
I bet they are doing some stupid string handling or something internally that is easy to fix but which causes O(n^2) performance, where n is the data size.
Note that I'm not chopping the results into pieces in the code above, but I experimented with that as well. I found that the optimal size was 1024 a chunk; going larger or smaller degraded performance.
With the chunking above, I could push 1.2 megabytes over in about 2 seconds; if I didn't use the optimizations above, it could easily hang the browser for a minute or more.
The other thing is you have to encode certain values (again because they are using XML and didn't use CDATA sections, so you run into the same double encoding problems that RSS 0.92 used to have):
testValue = testValue.replace(/\n/g, "\\n");
testValue = testValue.replace(/\r/g, "\\r");
testValue = testValue.replace(/\f/g, "\\f");
testValue = testValue.replace("'", "\'");
testValue = testValue.replace('"', '\"');
// double encode all entity values, or they will be mis-decoded
// by Flash when returned
var entityRE = /\&([^;]*)\;/g;
testValue = testValue.replace(entityRE, "&$1;");
So, we encode all new line types, which if we don't causes the values to be corrupted; we also encode JavaScript string terminators so that when Flash hands us back its values as JSON, that these terminators don't break the results (Flash also doesn't correctly encode JavaScript string terminators when it hands its results back. Sigh.) We also generically encode any kind of XML/HTML entity reference, rather than hard code specific ones (thats the &$1; thing up there).
In general, whoever coded ExternalInterface should be fired. Great idea, terrible implementation. Have they ever heard of unit tests and boundary conditions?
Anyway, here's the great thing about dojo.flash: you won't have to know about any of the tricks above. It encapsulates you from these things, so that when we don't need them anymore we can swap them out. I had gotten Flash 8 to acceptable performance that degraded nicely using the tricks above, and was all ready to throw out the Flash 6 stuff, when I found out that Flash 8 is not supported on Linux (and they pretty much just got Flash 7 for that platform). Damn.
Subscribe to Posts [Atom]