openfl.display.Loader.loadBytes() performance

I’m porting an old flash project to html5 using haxe/openfl. My application loads multiple images via server-side API, so I need to additionaly check each response for possible server-specific error (which comes in xml form). In flash, I was loading raw binary content using URLStream, then, after examining the content, a Loader.loadBytes() was used to load actual BitmapData.
Sadly, in OpenFL/HTML5, this method has quite poor performance (e.g. for 500Kb image, execution stalls for ~100ms). I’ve profiled the code and found 2 bottlenecks:
1. Base64 encoding. Problem is, rather generic encoder is currently used with an additional binary-to-string conversion. So, I’ve created a specialized encoder, which uses bitwise ops instead of arithmetics, does encoding in 3-byte chunks and assembles a final string using a big 64x64 lookup-table (string concatenation seems to the biggest performance issue here). In my tests, it shows ~4x speed improvement (16ms vs 67ms), so maybe you would want to include it into haxe/lime source.
2. __isSameOrigin() method in HTML5HTTPRequest. Takes ~20ms on my machine. I’m kinda new to js/html5 development, but i guess it has something to do with cross-site content loading. I wonder if that can be optimized for in-memory URIs? Like (pseudocode):
if (path.startsWith("data:")) return true;

Just incase, here’s a cleaner version of the encoder:

import haxe.io.Bytes;
import openfl.utils.ByteArray;

class Base64Codec {

	private static var DICTIONARY: Array<String> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
	private static var EXTENDED_DICTIONARY: Array<String> = {
			var result = new Array<String>();
				for (a in DICTIONARY) {
					for (b in DICTIONARY) {
						result.push(a + b);
					}
				}
			result;
		};
	
	public static function encodeByteArray(source: ByteArray): String {
		return encodeBytes(source);
	}
	public static function encodeBytes(source: Bytes): String {
		var result: Array<String> = new Array<String>();
		
		var dictionary: Array<String> = DICTIONARY;
		var extendedDictionary: Array<String> = EXTENDED_DICTIONARY;
		
		var numBytes: Int = source.length;
		var numInputTriplets: Int = Math.floor(numBytes / 3);
		var numChunksToWrite: Int = numInputTriplets * 2;
		untyped result.length = Math.ceil(numBytes / 3) * 2;
		
		var numBytesRead: Int = 0;
		var numChunksWritten: Int = 0;
		while (numChunksWritten < numChunksToWrite) {
			var inputTriplet: Int = (source.get(numBytesRead) << 16) | (source.get(numBytesRead + 1) << 8) | source.get(numBytesRead + 2);
			result[numChunksWritten]     = extendedDictionary[(inputTriplet >> 12) & 0xfff];
			result[numChunksWritten + 1] = extendedDictionary[(inputTriplet      ) & 0xfff];
			
			numBytesRead += 3;
			numChunksWritten += 2;
		}
		
		switch (numBytes - numInputTriplets * 3) {
			case 1:
				var inputTriplet: Int = (source.get(numBytesRead) << 16);
				result[numChunksWritten]     = extendedDictionary[(inputTriplet >> 12) & 0xfff];
				result[numChunksWritten + 1] = "==";
			case 2:
				var inputTriplet: Int = (source.get(numBytesRead) << 16) | (source.get(numBytesRead + 1) << 8);
				result[numChunksWritten]     = extendedDictionary[(inputTriplet >> 12) & 0xfff];
				result[numChunksWritten + 1] =         dictionary[(inputTriplet >>  6) &  0x3f] + "=";
			case _:
		}
		
		return result.join("");
	}
}

Thanks!

I’ve updated Lime with both of your improvements. You can give a dev version a try here if you like: https://github.com/openfl/lime#building-from-source

Great! I’m glad to contribute to this wonderful project! :slight_smile:

I’m pretty new to html5 platform, so I’m constantly learning about new things. Yesterday I found out that by far the best approach to load image from in-memory binary is URL.createObjectURL(). In my tests it takes a negligible ~1.5ms, which is a ~50x speed improvement over the initial ~100ms (in this case, even the old version of __isSameOrigin() takes no time at all, since an URL is short). An implementation is pretty simple: haxe.io.Bytes.getData() returns js.html.ArrayBuffer which is directly convertible to js.html.Blob. The only problem to take care of is post-load blob URI revocation. If we decide to integrate that into Lime, we need to decide who will be responsible for revocation: it should either be a promise created by Image class or HTML5HTTPRequest itself (but we need a mechanism to let it know that URI must be revoked after load completion/failure/cancellation).

Also, this API is not available in all browsers, so availability must be checked too. Right now, I’m doing this:

var localURI: String = generateLocalURI(bytes, contentType);
if (localURI != null) {
	// load blob URI
} else {
	// go ye olde base64 way
}

	private static function generateLocalURI(data: Bytes, contentType: String): String {
		if (URL != null) {
			if (URL.createObjectURL != null) {
				var blob: Blob = new Blob([data.getData()], {type: contentType});
				return URL.createObjectURL(blob);				
			}
		}
		return null;
	}

Is that a correct way to determine API availability?

That seems like a reasonable way to check if the API is available

Support for the feature seems pretty green (even back to IE 10) so we may be able to just use it without worry? We require JS typed arrays which removes support for IE 9 and other older browsers.

Perhaps there’s a way (in this case) to attach the revocation to the Loader object so calling loader.unload will revoke it (otherwise it will remain in memory)

Hehe, I’ve never used unload() in my 10 years of flash dev experience, because “why? GC will handle everything!”. Problem is, “blob URI” is 100% manual allocation (like malloc()/new in C/C++) and must be deallocated manually, too (think free()/delete), and since we have no destructors/finalizers in JS, we should revoke it as soon as we no longer need it (basically, on load completion - successful or not - and on cancellation).

Besides, I thought we should dig this improvement deeper into Lime, incase someone is using it directly (e.g. lime.graphics.Image.loadFromBytes()), so I took liberty and amended some classes (this time, I tried to match code formatting style :slight_smile: ).

Alas, to my great shame, I never worked in big teams, so I have no experience with neither git, nor github, so I hope you don’t mind if i deliver the code via other means - at least until I have some free time to wrap my head around git. :slight_smile: So, here’s the link.

P.S. What does “display” compiler macro stands for? In some cases its “#if (js && html5)” in other it’s “#if (js && html5 && !display)”

Same here with 20 years Flash dev experience :sweat_smile:

Thank you!

It’s slightly confusing but “display” is when the Haxe compiler is performing either code completion or generating our API documentation. We use this to guard platform-specific code at times that may break the doc tool (since every signature needs to return identically between compile targets).

You might consider trying either the Github Desktop app which is pretty easy to use or Github online will (nowadays) let you click an “Edit” button on a source file and make or paste your edits online then make a pull request right there.

As a totally minor thing we alphabetize our methods, but also we mostly use Visual Studio Code (with the Haxe and Lime plugins) which actually supports auto-code formatting on save, so it can put all the braces and spaces in the way the codestyle dictates just for consistency (it doesn’t match my personal code style personally but the automation is neat)

There’s also the OpenFL Discord which is a great place to chat as well :slight_smile: