Thursday, November 09, 2006

AJAX Submission Throttling

Submission Throttling:

HTML Elements: Text field, Select box or any other control

Events: On key up /down

Technology used: AJAX and JSON response. (XML or text can also be used)

JSON: more information on http://www.json.org/

1. Naive Approach: The initial implementation of getting data for every key press was not a viable option to live with due to performance reasons. Since every request for individual letter typed in by the user causing delay in showing the desired data, the submission throttling method was adopted to address this issue.

2. Constant Time delay

The initial solution identified was to wait for the fixed delay (2 seconds, or similar), and submit the request there after.

For example: When user types the search criteria, irrespective of the speed in which user has typed the criteria, the request was submitted after the fixed delay.

Default Delay:


The delay mentioned in the above stated example is constant. The problem with the default delay was that the “fast typer” will have to wait minimum of the default delay introduced by the system. Hence the solution was to introduce a varying delay.



Differential delay (Based on typing speed)


Varying delay:

The system will responds based on the typing speed of the user. Following is the series of steps in which the submission throttling is implemented.

Breathe time (BT):

This is the default delay set by the system. App has the delay of 150 ms seconds to start with.

Actual Elapsed Time (AET):

This is the time duration between the typing of a character and the next event. Delay will be calculated based on the BT + time gap between typing last character and the current character.

Default Elapsed Time (DET):


The DET is set to constant value (3*BT), However, Introduced delay will not exceed the DET. This is because user may be knowing first few character and then after typing that he will wait for server to tell the matches for the string he typed.

Method to watch below

XHR.abort
scopeThis
calculateElapsedTime
callServerScript





var noOfReq =0;
var noOfAbort =0;
var noOfCancel =0;
var noOfServerCall =0;
var totalCall =0;

function XHR (advSuggest) {
this.req;
this.advSuggest = advSuggest;
this.isProgress= false;
this.isCompleted = false;
this.isAborted = false;
this.init= function (){
++noOfReq;
// // alert ("noOfReq=" + noOfReq);
try{
this.req=new ActiveXObject("Microsoft.XMLHTTP");
}
catch(e){
try{
this.req=new ActiveXObject("Msxml2.XMLHTTP");
}catch(oc){
this.req=null;
}
}

if(!this.req && typeof XMLHttpRequest!="undefined"){
this.req=new XMLHttpRequest();
}

};

this.setProgress= function (isProgress){
this.isProgress= isProgress;
};
this.setCompleted= function (isComplete){
this.isCompleted = isComplete;
this.setProgress(true);

};
this.resetReadyState= function() {
this.req.onreadystatechange = function () {};
};
this.abort= function() {
if(this.req.readyState>1 || this.req.readyState <4){
try{
this.resetReadyState();
}catch(e){
}
this.isAborted = true;
++noOfAbort;
this.req.abort();
}
};

this.makeRequest = function (inputText){
var url = "./fetchAdv.jsp?text="+escape(inputText);

if(this.req!=null){
this.isProgress = false;
//this.req.abort();
this.req.open("GET", url, true);
this.req.onreadystatechange = scopeThis (this, this.handle, inputText);
try{

this.isAborted = false;
this.req.send(null);
++noOfServerCall;
}catch(e){
alert ("Exception while aborting - " + e);

}
this.isProgress = true;

}

};
this.handle = function(inputText) {
if(this.isAborted === false && this.req.readyState === 4){
try{
if (!this.req.status || this.req.status === 200 ){
var advnames = eval("[" + this.req.responseText + "]");
this.isCompleted= true;
this.isProgress = false;
this.advSuggest.process (advnames, inputText);
return;
}

}catch(e){
alert("Exception " + e +"\n");
}
}

//return empty, is it required?
//this.advSuggest.process ("");

};

}


function AdvSuggest () {
this.req;
this.prevval = "";
this.inputtxt = "";
this.timerid="";
this.prevtime = 0;
this.suggestshown = 0;
this.focusout = 0;
this.searchdiv = document.getElementById('searching');
this.canSubmit = false;
var BREATH_TIME = 150;
var ELAPSED_TIME = BREATH_TIME *3;

var SELECTION_HTML = “";


var CACHE_RESULT = [];

this.printCache= function (){
return CACHE_RESULT;
};


// This function cleases string passed by de-specialising the "_" and "+" characters.
function cleanseString(str) {
var processedstr=str.replace("_","\\_");
processedstr=processedstr.replace("+","\\+");
return processedstr;
};

// This function calculates the time elapsed since the last key in by the user.
function calculateElapsedTime () {
var currentTime = new Date().getTime();
var currentTimeDiff = 0;
this.prevtime = this.prevtime ? this.prevtime : currentTime ;
currentTimeDiff = currentTime - this.prevtime + BREATH_TIME;
this.prevtime = currentTime;
return (currentTimeDiff === BREATH_TIME ) ? ELAPSED_TIME : (currentTimeDiff > ELAPSED_TIME) ? ELAPSED_TIME: currentTimeDiff ;
};

// This function initialises the XMLHttp objects for the Ajax call.
this.initialize= function (){
if(!this.req){
this.req = this.req || new XHR (this);
this.req.init();
}
};

// This function handles the keyup event on the advertiser search field appropriately.
this.callServerScript= function (searchstring, evt){
this.inputtxt= cleanseString(searchstring);

if(evt.keyCode == 40 && this.suggestshown){
this.showAdvertiserDetails('autocomplete');
document.search.autoselect.focus();
}


if(this.prevval == this.inputtxt){
return 0;
}

this.prevval = this.inputtxt ;

if(this.timerid != ""){
clearInterval(this.timerid);
++noOfCancel;
}

if(this.inputtxt == ""){
this.cancelRequest();
this.hideAdvertiserDetails('autocomplete');
return 0;
}

if(this.containsValue(this.inputtxt) ){
this.cancelRequest();
this.setContent(this.getValue(this.inputtxt)) ;
return 0;
}
this.cancelRequest();
var elpsTime = calculateElapsedTime();
this.timerid=setTimeout(scopeThis(this, this.getDetails), elpsTime);
++totalCall;

};

this. getDetails = function (){
this.timerid = "";
this.initialize();
if(this.req!=null){
this.req.makeRequest(this.inputtxt);
}
};

// this function checks if the keyed in text was already cached.
this.containsValue = function (key){
key = key.toUpperCase();
return CACHE_RESULT[key]? true: false;
};

// This function adds the keyed in value to the local cache if it is entered for the first time.
this.addValue = function (key, value){
key = key.toUpperCase();
CACHE_RESULT[key] = value;
};

// This function is used to get the keyed in value from the local cache.
this.getValue = function (key, value){
key = key.toUpperCase();
return CACHE_RESULT[key];
};

// This function is used to display or hide the returned data from the server side script.
this.process= function (advnames, searchText){
this.addValue(searchText, advnames);
this.setContent(advnames);
};

function toHtmlSelectString (data) {
var suggestctrl="";
var optionStr="";

if( data != ""){
suggestctrl = SELECTION_HTML;
for(var advname in data) {
// generate/form options html fragment
}
suggestctrl = //final select html;
}

return suggestctrl;
};

// This function is used to display the fetched data to the select box in the front end.
this.setContent = function (data){
var contentHtml = toHtmlSelectString (data);
// Push it to innerhtml
};

// This function is used to formulate the position of the suggestionbox on the search page.
this.findDisplayCoordinates= function (obj){
var curleft = 0;
var curtop = 0;
if (obj.offsetParent){
curleft = obj.offsetLeft;
curtop = obj.offsetTop;
while (obj = obj.offsetParent){
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
}
}
return [curleft,curtop];
};

//This function is used to handle the refreshing of the request object for firefox.
this.cancelRequest= function (){
if(this.req) {
try{
// Make it Async
setTimeout(scopeThis(this.req, this.req.abort), 0);
// this.req.abort();
}catch(e) {

alert ("Exception while aborting - " + e);
}
}

};
}

// This function assigns the properties to the passed object.
function scopeThis (obj, method) {
var startIndex = 2;
var args = [];
for(var x=2; x< arguments.length; ++x) {
args.push(arguments[x]);
}

return function () {method.apply(obj,args);};
};




Testing

// instantiate the AdvSuggest object in body onload
var advSuggest = new AdvSuggest ();

// Create input text and div element

then onkeydown make a call to
advSuggest.callServerScript(this.value,event)
Change the URL in the XHR object


I omitted most of the event handling like show/hide etc.