Challenging Preconceptions With Extreme JavaScript And AJAX
Massive advances require massive challenges. The developers of the XB-70 produced a plane capable of flying three times the speed of sound and taught the engineers (and eventually the world) many vital lessons in structural design and avionics. Why am I using this as an example for I.T.? Well – just look at the thing! It looks like nothing that ever came before it, and quite frankly still looks like science fiction compared to the lack-lustre, play-it-safe aircraft of the modern era.
What we in IT can learn from this is: Whilst playing it safe gives us a nice warm feeling, only by pushing our limits will we achieve world beating results! In this article, I will show you how going way beyond conventional wisdom with JavaScript and AJAX can yield spectacular results and reliability that transcend all expectations. Yes – you read that correctly – challenging ideas do not necessarily result in unreliable products. If an approach is inherently superior to its predecessors, it can both be new and more reliable! The techniques described in this article can be seen at Cubical Land Games and in DeployView. If you want to see source code, you'll need to go to Cubical Land. This shows an advanced real time game using standards compliant Javascript, CSS and HTML:
Step 1: Defensive JavaScript Programming
Most programmers are brought up with (or learn from books) the idea of 'it should work'. That is to say, a programming environment is stable and predicable: A modern computer is a deterministic digital device, each state is a direct consequence of the previous state and so everything should always work the same and you can rely upon each piece working properly all the time. This concept is totally wrong in an Internet environment! Rather than the usual approach of dismissing some piece of code or even an entire browser as 'no good' because it sometimes does wrong things, we have to learn to make program systems that live with failure. No one functional unit should ever be totally dependant on another functional unit working faultlessly 100% of the time. Once you have managed to get your head around coding for failure (defensive programming), you have won the hard part of the battle!
If the very idea of coding for failure is just offending the heck out of you, let us look at some very strong examples of it 'out in the wild'. Probably the best example I can think of comes from modern operating systems. Have you ever wondered by older PC type operating systems used to crash constantly but newer ones stay up reasonably well. Consider Most-Applications-Crash-If-Not-Operating-System-Hangs (sorry Macintosh OS) which crashed with a remarkable regularity whilst OS X is very stable. Also, Windows ME had a truly amazing ability to crash where as Windows XP will stay up long enough to save your work before it dies (normally).
So, why the difference? Well, OS X and XP are 'separate kernel' operating systems. The programs that you run are separated from the operating system core (referred as the kernel). The kernel is designed to tolerate user applications crashing; it just cleans up after them. In old style operating systems the core relied on user applications cleaning up after themselves. If a user application did something wrong it could easily bring the whole operating system down. In other words, modern operating systems are coded for failure!
Step 2: Event Loops And Object Pools
Modern web browsers (I have used these techniques in IE6, IE7, Firefox 1.5 and Konqueror – which means they should also work in Safari) are complex multi-threaded environments. Unlike programming in C, .net or Java, you do not have proper control over the threads in a script running inside a browser. The solution to this conundrum is to program in a thread defensive fashion. You make a program that should stay stable in the face of unexpected threading interactions. The best model I have found for this style of programming is to use event loops and object pools. Do not get me wrong, I am not going to bang on about object oriented programming here, it is just that in this case a sparing use of a few objects can be very handy!
The basic concept of an event loop is that you one 'root' section of code that is run over and over again. This section of code then dispatches 'events' out to other piece of code as and when appropriate. This is a common model in computer programs – especially those employing graphical interfaces. The pieces of code to which events are despatched are called listeners. For Javascript I employ a slight adaptation to the idea. Rather than the looping code having some form of logic that discriminates as to which listener should get which event, I make the event loop very simple indeed. It just 'wakes up' all the listeners periodically and they work out if they should do something.
/* This code handles events by dispatching activity to 'listener' objects. All that is
* required of the objects is that they implement a .fire() method. This is the
* method which gets called each time the event loop 'wakes up'.
*/
/* We use two pools. One holds events to run and the other events to remove.
* an event listener will choose when to be removed by calling RemoveEvent(this),
* however doing so whilst the event loop is looping through the event listeners
* could corrupt the event listener pool. Also, mixing activities of
* deleting events with firing events is not especially stable when multiple threads
* are flying around. So, we store up all those events which are going to be
* removed, and then remove them all after the events have all been fired.
*/
var eventPool=new Array();
var removePool=new Array();
/* Each even object is given a unique id, this helps with house keeping. */
var eventId=0;
/* As we live in a complex, multi-threaded environments, it is much more stable
* to put core data access algorithms inside a function which other functions
* can then call. Also note that there must never be ten million events at once
* so it is safe to wrap the ID at this point to avoid the (probably mythical)
* chance of numeric overflow.
*/
function GetNewEvId()
{
return eventId > 10000000 ? 0 : eventId++;
}
/* Again, it is much more stable to put data manipulation and algorithms that
* manage core activities in functions. This one adds an event listener to
* the pool of event listeners
*/
function AddEvent(o)
{
o.eventId=GetNewEvId();
eventPool.push(o);
}
/* This function queues an event listener for removal from the event pool.
*/
function RemoveEvent(o)
{
removePool.push(o);
}
/* This function (not the best name in the world) removes a particular event from
* the event pool. It would be possible to create an algorithm that removes all of
* of them at once and scales better than this approach, but as removal is very much
* rarer in this application than firing, it was not worth the extra complexity.
*
* The fact that all event listeners are given a unique ID comes into play here, because
* the algorithm can be passed the object to remove and then recognise it in themselves
* event pool from its ID.
*
function RemoveEvents(o)
{
var id=o.eventId;
var n=new Array();
var l= eventPool.length;
for(var i=0;i
{
var oo=eventPool[i];
if(oo.eventId!=id)
{
n.push(oo);
}
}
eventPool=n;
}
/* This is the core method of the event queue. It is called from the 'onload'
* event of the element of the document. From then on, it sustains
* the event loop by its self. It performs events based on a 'frame rate'
* The frame rate is actually a maximum frame rate as it is set by a delay
* between firing all the events and firing them again. The more time firing
* the events takes, the slow the actual frame rate.
*
* It might seem more logical to set a timeout for each event individually, but
* this does not work as you might expect. What happens is that the browser will
* choose when to actually run the methods passed to 'settimeout'. It may run
* at any point after the timeout set. Under heavy load this tends to result
* in nothing happening for a while and then a bunch being run in short succession.
* In a game environment this is a disaster because the game become 'jerky'.
*
* By reducing our reliance on the timing of the browser (defensive programming)
* we are making the code behave more like we want it to under adverse conditions.
*
* Once all the events are fired, any events that are queued for removal are
* removed.
*/
function DoEvent()
{
var l= eventPool.length;
for(var i=0;i
{
eventPool[i].fire();
}
l=removePool.length;
for(var i=0;i
{
RemoveEvents(removePool[i]);
}
removePool=new Array();
setTimeout('DoEvent()',timeoutValue);
}
This picture is here because there is too much text in one lump otherwise :-)
Now that we have a few tricks to make stable timing loops and queues in JavaScript, we can use these ideas to improve the way we use AJAX: AJAX actually admits it might go wrong! A web server is perfectly within it rights to not serve a page for any reason. The main reason is overload, though many others exist. This means that we should 100% absolutely never rely of a AJAX call returning with anything meaningful! If we require some degree of reliability using AJAX, defensive programming for failure is a must.
Here is a simple example of updating a database using AJAX and reflecting the results of the update back to the user. The Code has been cut down in places so that the core parts are easier to see.
/* The concepts in the previous example are applied here as well:
* Communication with the server via AJAX is considered to be an
* event. The event is fired periodically until a successful return
* comes from the server.
*
* This system accumulates instructions to invert the status of a day
* and submits the instructions once every x milliseconds (currently 500).
* If there are no inversions to submit - it does nothing.
* This means that lots of individual 'clicks' are grouped together
* and so prevents server and javascript overload.
*/
var invertQueue=new Array();
/* As before, we place updates to core data structures in functions */
function pickerAddElemToInvertQueue(elem)
{
invertQueue.push(elem);
}
/* This function tries to update each item that is stored in the
* the invert queue. We do not hold listener objects in this
* example as we do not require that much flexibility. All we
* ever do it update or check the status of particular Ids. So
* queues just hold the IDs.
*
* Also note that if an even actually works it is always removed
* from the queue. This leads to the architecture here where
* we empty the queue and then refill it with any failed events.
* However, the process has two steps just like in the previous
* example – so is still stable.
*/
function pickerCheckInversionQueue()
{
if(invertQueue.length>0)
{
// use a loop to drain the queue to ensure there are
// no multi-threading issues
var toInvertList=new Array();
while(invertQueue.length>0)
{
toInvertList.push(invertQueue.shift());
}
// Send the toInvertList to the server and get (hopefully) a
// reply for each one
var rpcObj=new XmlRpcClient('--A URL --');
rpcObj.debug=false;
try
{
// Get the list of utc second 'days' to
// invert from the id's of the elements
var utcList=new Array();
for(var i=0;i<toinvertlist.length;++i)
{
utcList.push((new String(toInvertList[i].id)).substr(4));
}
var reply=rpcObj.execute
(
'InvertWorkingStatus',
pickerGetLocationId(),
utcList
);
var working=reply.Working;
var nonWorking=reply.NonWorking;
for(var i=0;i<working.length;++i)
{
/* ... Code To Update Document's DOM ... */
}
for(var i=0;i<nonworking.length;++i)
{
/* ... Code To Update Document's DOM ... */
}
}
catch(e)
{
/* ... Code to send debug to user if wanted ... */
}
// Failed to update, so put them back on the queue
while(toInvertList.length>0)
{
pickerAddElemToInvertQueue(toInvertList.shift());
}
}
}
}
/* This function creates the event loop. It fires all the update
* events, then calls its self after a 500 milliseconds.
* The first call to this function must come from the 'onload'
* event of the document body.
*/
function pickerCheckQueues()
{
pickerCheckInversionQueue();
window.setTimeout('pickerCheckQueues()',500);
}
Contrast To Raw AJAX
This structured, controlled and resilient approach to AJAX could not be more different to the standard 'fire and forget' mess that has started to give this powerful Web techniques a dodgy reputation...
Can you spot the difference:
Which one is real and which is a cruddy kit car?
One of the above is the real thing. The other has a superficial resemblance to the real thing, but will disappoint! This is the difference between a system designed to function in the unstable Web/AJAX world and one that has been 'just built'.
Let us review the differences between the approach given here and 'just build it' AJAX:
First off – the AJAX example given does not rely on the HTTPRequest its self to provide asynchronisity. Indeed, the HTTPRequest as sent synchronously. The code uses the 'settimeout()' built in JavaScript function to run the AJAX in a separate thread of activity. If one was to launch (as is often done) many AJAX calls at once, and rely on the browser to provide asynchronous call back, one has no control over the order in which the calls will return. There is also a good chance that you will have multiple threads colliding in the functions which update the core data structures. This will lead to corruption of those structures and system failure.
Secondly – this approach has an ordered failure management system and an effective mechanism for retry of failed calls. Compare this to catching failures in a callback function. In that more primitive approach, each time there is a failure one would raise a new request straight away – which could potentially raise a 'request storm' on the server. If you were to place the re-request in a settimeout() then you could have vast numbers of settimeout() calls running at once, which will produce very poor performance in the browser or even resource exhaustion.
Thirdly – this system is extensible. Consider that we found the 500 millisecond gap too short where the server was overloaded. We could add in an algorithm by which the gap increased should more than a cut off number of failures occur. This would mean the system as a whole (clients and server) could co-operate in achieving maximum possible performance. Such client/server interactions are 100% necessary if we are to achieve the sort of flexible, responsive Web experience we all want.
Now that we have a stable architecture for AJAX, we can push the limits of what we do with it; we can make truly interactive web pages where multiple people can communicate with a central system in real time!
A Final Thought
Below is an enhanced version of a long distance photograph showing the end of AV/2, one the the only two XB70's ever built (sometimes called the YB70). The pilot of the chase plane which struck AV/2 and one of the two pilots of the experimental aircraft its self lost their lives because of this mid-air collision. Whilst this was a tragedy, they were test pilots and so taking risks was part of their job. People like these risk their lives to help progress technology.
In the modern era we seem to have become obsessed with the alleviation of risk. In the IT industry, we only take steps which are highly predictable. That will only ever produce predictably under-performing results. I hope that in this article I have shown that by taking a more radical approach to existing techniques, we can push forward and stop wasting time with 'OK' solutions. We should grasp the nettle and make amazing solutions!
Dr Alexander J. Turner





2 comments:
My javascript is a little rusty, but isn't it true that this *always* returns 0 after the eventids hit the barrier?
That's why I don't like defensive coding: execution errors are much easier to debug, defensive coding adds a layer of complexity to your program that more often than not gets in the way. If the barrier is never reached as you say, that makes the case against checking for it only stronger.
Having said that, I have had to code defensively, I understand the world isn't always the way we'd like it to be, especially when targeting browsers. ;)
I cannot remember well enough now (blush). My guess is that I am being defensive here because I want it make sure it works in different browsers with different versions of JS.
I am with you on the defensive programming side of things. If I am coding in a very well understood environment then I prefer to tune everything to be correct and thus finding errors is better than hiding errors. As you point out, this code runs in a browser and so is not in a well understood environment.
The other place I always prefer defensive programming is in security concious code where someone might be trying to make it behave in incorrect ways.
Thanks for the comment - AJ
Post a Comment