Post image for Fun with meld.js and AOP in Titanium JavaScript

Fun with meld.js and AOP in Titanium JavaScript

by Bill Dawson on 21 May 2013

If you have played around with Aspect-oriented Programming (AOP), you are familiar with the idea of “cross-cutting concerns”. These are the kinds of things you almost always have to do in your programming work but which are typically a PITA when it comes to trying your best to keep your code modular and respectful of the Law of Demeter.

Logging and instrumentation are the classic examples. During testing (or even in production) you might want to log every method call and/or check the performance of key parts of your program. But it’s painful to start cluttering up your wonderfully modular objects and methods with lines of code which are not relevant to their core concerns. If you have a method which deducts a withdrawal from a bank account balance, you want every line of code in that method to be relevant only to deducting from the balance. You don’t want to pollute the method with log statements or instrumentation code, neither of which would be considered a core concern.

Enter AOP and the various programming language extensions which help you implement AOP in your projects. The most famous of all AOP extensions is AspectJ, the Java extension developed by the group at Xerox PARC which brought AOP into the mainstream. For Titanium Android JavaScript, I’ve been having a look at meld from the cujoJS folks. Below is a very simple example of using meld for precisely what I was hoping to do, namely measure elapsed times for HTTPClient calls without putting much (if any) measurement/instrumentation where it doesn’t belong.

I’ve torn the HTTPClient call code out and put it into an app.js for simplicity’s sake. It grabs the appcelerator.com front page and finds the number which Appcelerator puts there that indicates how many devices are out there running Appcelerator-powered apps.

As you see, there’s just a constant to indicate if instrumentation should be done at all, then one line of code to enable instrumentation on an instance of the HTTPClient:

const INSTRUMENT = true;
var xhr = Ti.Network.createHTTPClient();

xhr.onload = function(e) {
	var match = /<h1.*>([0-9,]+)<\/h1>\s*<h4.*>devices running/g
			.exec(xhr.responseText);
	
	if (match && match.length >= 2) {
		console.log(match[1] + " devices are running Appcelerator-powered apps.")
	} else {
		console.log("Could not determine how many devices are running Appcelerator-powered apps.");
	}
};

xhr.open("GET", "http://www.appcelerator.com", true);

if (INSTRUMENT) { require("/instrument").instrument(xhr); }
xhr.send();

That instrument module which is require()'d in that code above is this:

var meld = require("/meld");

exports.instrument = function(xhrInstance) {
	var xhr = xhrInstance,
		startTime,
		readyTime;

	function meldBefore() {
		if (!startTime) {
			startTime = new Date();
		}
	}

	if (Ti.Platform.osname === 'android') {
		// In Android, the "send" method is available
		// directly on the JavaScript side of the bridge.
		meld.before(xhr, "send", function() { startTime = new Date(); });
	} else {
		// Otherwise, the best we can do is assume
		// you'll call .send right after instrumenting,
		// so log the startTime right away.
		startTime = new Date();
	}

	meld.before(xhr, "onload", function() { readyTime = new Date(); });

	meld.after(xhr, "onload", function() {
		var now = new Date(),
			networkElapsed = readyTime - startTime,
			handleResult = now - readyTime,
			totalElapsed = networkElapsed + handleResult;

		console.log("XHR performance for url '" + xhr.location + "':");
		console.log("   Network: " + networkElapsed + "ms");
		console.log("   Handle result: " + handleResult + "ms");
		console.log("   TOTAL: " + totalElapsed + "ms");
	});
};

Note the calls to meld.before and meld.after, which “advise” the appropriate methods. See the meld reference for precise descriptions of what those do. The end result is these log statements:


I/TiAPI   ( 9406):  134,567,882 devices are running Appcelerator-powered apps.
I/TiAPI   ( 9406):  XHR performance for url 'http://www.appcelerator.com':
I/TiAPI   ( 9406):     Network: 2188ms
I/TiAPI   ( 9406):     Handle result: 35ms
I/TiAPI   ( 9406):     TOTAL: 2223ms

If I were to ship that code, I could simply set the INSTRUMENT variable in app.js to false and that would be that. Or I could have a user preference inside the application, which I could tell the user to set if I’m following up on a support issue, particularly if I have beta users.

If we look back at the app.js, we see that I didn’t completely accomplish my goal of having no instrumentation code in or near my code that uses the HTTPClient: there’s still that if statement that checks INSTRUMENT and calls the instrument module accordingly. So I could make this even better by making my own constructor function (i.e., “class”) which would perform all of the useful work (the HTTPClient stuff) and not contain any instrumentation code. Then I could use meld’s facility to advise constructors to wire up some instrumentation. The “wiring up” would occur completely outside the class definition itself, for example in the app’s startup code.

meld (and AOP) would also be very useful for evaluating code coverage. You could advise every method in your class/instance to be logged as they get called, then run unit tests and later check the log to see what didn’t get called. Here’s an example of instrumenting every method (this is a copy/paste from the Node REPL):


> var o = {func1: function() {console.log("You called func1!");},
... func2: function() {console.log("You called func2!");}};
undefined
> meld.before(o, /.*/, function() {console.log("Before calling " + meld.joinpoint().method);});
{ remove: [Function] }
> o.func1();
Before calling func1
You called func1!
undefined
> o.func2();
Before calling func2
You called func2!
undefined
> 

Note the regex /.*/ passed to meld.before, indicating that every method should be advised.

In summary, I think meld has a lot to offer for dealing with cross-cutting concerns in your Titanium JavaScript (or any JavaScript) code. You will find that it's easier to use meld to advise your own class/object methods as opposed to Titanium API methods, since the JavaScript type systems are different between Titanium platforms. If you have another look above at my instrument.js code, you'll see what I mean: there is a section there where I advise Android's HTTPClient.send, but I cannot do that in Titanium iOS.

(UPDATED 21 May 2013 to use const INSTRUMENT = true; instead of var INSTRUMENT = true; in the app.js code sample. Hat tip Matt Apperson).

Comments on this entry are closed.

Previous post: