In World of Raids Guild Recruitment Tool, we wanted the search to be instant! We achieved this goal by not having a single page refresh or ajax call while updating the settings. Since it runs in the browser, performance was a real concern. We will see in this article what were the tricks used to make this real.

Sorting

In order to sort the guilds, each guild is given a matching score (between 0 and 65535) based on the current filters. At start, each guild has the maximum points. Then, each filter is giving a malus. For example when a specialization doesn’t match, the guild loses 10%.

This means that everytime you change a filter, you have to recompute that matching score for all the guilds. Instead of recomputing the whole matching score, the guild holds the result of each filter. Everytime a filter is updated, the value of this filter is upated on every guild. The global matching score is now recalculated by summing all the partial precomputed scores.

Every bit of code in the matching function has been optimized, no more jquery code, loops are unrolled and great use of lazy boolean expression evaluation. The sort function itself also got boosted thanks to a trick I commented on my previous article Speed Up Javascript Sort().

Guild Recruitment Search Filters

Display

Once we have the data sorted, it has to be displayed! And this is another big time eater. When it takes 50ms to sort the data, it takes about 150ms to display 10 guilds!

Since we wanted performances, DOM elements are created at page loading, they are then only being updated as the guilds change. And still, only the required changes are made, for example tooltips are created as the user trigger the mouseover event.

The main problem with this is that we are not able to make bulk changes. Everytime you make the slightest change, the whole UI has to be redrawn which is a waste of time. I hope there will be such API in the future.

Guild Display

Since it’s a web application, we had to maintain the url up to date, this is done using SmallHash, a small library of mine that compresses filter data into the smallest possible hash.

Scaling

Having all the data being sent at page loading is a pain, it makes it load slowly. One way to avoid this problem would be to use the localstorage to keep these data accross sessions and only send the updated guilds. However at the time I started developping the application there was no released browser supporting HTML5.

However, the application is focused on World of Warcraft guilds. This is a limited audience and there is no need on heavy scaling there. So we decided that it was a good option to go.

Up to 3000 guilds, this is pretty much instant in every browser in my MacBook Pro. When increasing that number to 10 000 it is taking more than a second to update on IE8. It requires 50 000 guilds for Chrome 3 to get slow, which is a pretty good score. It means that it is possible to do heavy processing, the bandwith now being the limiting factor.

Working on the World of Raids Recruitment Tool we wanted automatic saving while editing. There are basically two ways of handling the problem.

  • Send update everytime something changes. The main advantage of this technique is that you are always up to date. However, it is spamming the server on fields that are constantly being updated (text fields for example).
  • Send update every X seconds. There is no longer query spike but there are two main disadvantages.

    The delay the data are sent after a modification can vary from nothing to the timer length for no apparent reason. This is a really bad feeling for the user. He will think that the application is buggy.

    The application is going to send queries all the time, even if there are no changes. We can safely assume that people are interacting a lot less than viewing so this is a real waste of resources.

We are interested in the first one, we want instant update but removing the ability to send too much data at once. What we want is to send data right after the user is done typing instead of everytime a character is typed. There is an easy way to do it in Javascript.

When you type the first character, it will start a timer of let say 100ms. Then everytime you type another character it resets the timer to 100ms. When it triggers, it will send data to the server.

This way, data will only be sent when the user interacts and with no risk of spamming the server.

var Object = {
  /* Object Code ... */
 
  timer: null,
  sendChanges: function() {
    if (this.timer) {
      clearTimeout(timer);
      this.timer = null;
    }
 
    this.timer = setTimeout(
      function () { /* Send Changes */ },
      200 /* Timeout in ms */
    );
  }
};

timer: null,
sendChanges: function() {
if (this.timer) {
clearTimeout(timer);
this.timer = null;
}

this.timer = setTimeout(
function () { /* Send Changes */ },
200 /* Timeout in ms */
);
}
};

Javascript – Type Casting

Comparison

In Javascript there are 3 types we are often comparing: String, Number and Boolean. After digging through the ECMA-262 specifications, here is the behaviour of the == operator (11.9.3) on these types:

  • Number == String
    Typecasted as follow: Number == Number(String)
  • Number == Boolean
    Typecasted as follow: Number == Number(Boolean)
  • String == Boolean
    Typecasted as follow: Number(String) == Number(Boolean)

This means that when comparing data of two different types, they will first be converted to Number.

Note: The order of the equality is not important: A == B is the same as B == A (except the order of evalution of A and B).

You can force comparison of a and b with the type you want:

  • String: "" + a == "" + b
  • Number: +a == +b
  • Integer: a | 0 == b | 0
  • Boolean: !!a == !!b

Addition / Concatenation

The binary + operator follows a simple rule (11.6.1):

  • if one of the operands is a String, both operands are converted to String and the + is a concatenation.
  • Else, both operands are converted to Number and the + is an addition.

Note: Some Objects are considered as Strings like Arrays. See 8.6.2 for more informations.

Note: The operator is binary so 1 + 1 + 'a' will be executed as (1 + 1) + 'a' and therefore be equal to "2a" and not "11a".

Test – Are you a Javascript Guru?

Javascript is a very flexible language, I made a compilation of some edge cases that you may have encountered while programming. The main goal is to point out some interesting specific behaviors.

Concatenation

1] var result = [10] + 1;
Explanation:
The Array doesn’t have a toNumber method, instead it has a toString method: [10] + 1 becomes “10” + 1.
String has a greater priority over Number with the + operator: “10” + 1 becomes “10” + “1” = “101”
2] var result = ['a', 'b', 'c'] + "";
Explanation:
The Array.toString method concatenates all it’s elements (converted to string) with the “,” separator.
3] var result = 'a' + 5;
Explanation:
In Javascript, String has a greater priority than Number for the + operator, so 5 is converted to string before being concatenated.
In PHP, the + operator is only the addition so 'a' is converted to number (gives 0) before being added.
In C, a char and an integer are of the same type. 'a' is first converted to it’s Ascii value (gives 65) before being added.

Operations

4] var result = 3.75 | 0;
Explanation:
|0 is a fast way to do a Math.floor for positive integers.
5] var result = 65 / 'a';
Explanation:
/ is only defined for Numbers so 'a' is first converted to a number (gives 0).
A division by 0 does not throw an error but results the object NaN (Not a Number).

Objects

6] var ob = {"10": 1};
ob[10] = 2;
ob[[1, 0]] = 3;
var result = ob["10"] + ob[10] + ob[[1, 0]];
Explanation:
Objects keys are strings. If you don’t provide a string, it will convert the key to string.
"10" and 10 gives "10"
[1, 0] gives "1,0"
7] var $ = {"": String};
var result = !!$[([])]();
Explanation:
$ is a valid variable name.
Parenthesis inside the key part are useless: array[(1+2)] is the same thing as array[1+2].
Objects key are first converted to string. [].toString() is "".
It is possible to have the empty string "" as an object key.
$[""] is the String object. String(val) returns val.toString(). String() returns "".
! is the negation operator. "" == false, so !"" == true, !!"" == false
!!expr is a fast way to typecast an expression to a boolean.

Equality

8] var result = (' trn ' == 0);
Explanation:
Unlike PHP, strings wrapped around simple quote ' are also parsed. 't' == "t"
When compared to a number value, a string will be converted to a number. If it contains only spacing characters it will be converted to 0. If a string cannot be parsed as a number it will return NaN (Note that NaN != NaN).
9] var a = new String("123");
var b = "123";
var result = (a === b);
Explanation:
When creating an object with the new operator, the result type is always "object". The type of "123" is "string" so the type does not match for ===.
10] var a = {key: 1};
var b = {key: 1};
var result = (a == b);
Explanation:
Object comparison is only done with the pointers behind the objects.

#jsguru h3 { margin-top: 20px; margin-bottom: -15px; }
#jsguru .wp_syntax { margin-top: 20px; }
#jsguru .result { margin-top: -16px; margin-left: 10px; }
#jsguru .hint { display: none; }
#jsguru label { font-family: monospace; }
#jsguru input { padding: 0 10px; }

var jsGuru = (function () {
var goodAnswers = [1, 2, 2, 2, 2, 1, 0, 0, 1, 1];
return {
test: function () {
var count = 0;
$.each(goodAnswers, function (id, val) {
if ($(‘[name=result’+(id+1)+’]’).eq(val).attr(‘checked’)) {
count++;
}
});
$(‘#jsguru-result’).html(‘You scored ‘ + count + ‘ / 10’);
},
highlight: function () {
$(‘#jsguru .hint’).show();
$.each(goodAnswers, function (id, val) {
$(‘[name=result’+(id+1)+’]’).eq(val).parent().css(‘font-weight’, ‘bold’).css(‘color’, ‘orange’);
});
document.location.hash = ‘jsguru’;
}
}
}());

The bracket notation for string is incomplete in Javascript and does not work in IE7. This is really painful to migrate to the .charAt(pos) equivalent, this is why i recommend you not to use it.

// Bracket Notation
"Hello World!"[6]
// > "W"
 
// Real Implementation
"Hello World!".charAt(6)
// > "W"

// Real Implementation
"Hello World!".charAt(6)
// > "W"

The bracket notation to get a character from a string is a shortcut to .charAt(pos) added by the vast majority of the browsers. However, i would not recommend to use it for several reasons.

This notation does not work in IE7. The first code snippet will return undefined in IE7. If you happen to use the bracket notation for strings all over your code and you want to migrate to .charAt(pos), this is a real pain: Brackets are used all over your code and there’s no easy way to detect if that’s for a string or an array/object.

You can’t set the character using this notation. As there is no warning of any kind, this is really confusing and frustrating. If you were using the .charAt(pos) function, you would not have been tempted to do it.

var string = "Hello World!";
string[6] = '?';
console.log(string);
// > "Hello World!";

Speed Up Javascript Sort()

By overriding the toString Object prototype, it is possible to speed up by 5x the sort function. This is an easy to implement trick that gives astonishing results

I wanted to know if there were ways to speed up the Javascript Sort function. I came across an interesting article (Yet another faster Javascript Sorting) that presents a way to boost the builtin sort function. However, the link with the detailed explanation is dead, so i make you a summary here.

To sort some data, you are likely to do something that looks like that:

data.sort(function (a, b) { return b.key - a.key; });

The comparison function is being called n log n times. Since it’s a javascript function, it is slow. sort() with no parameters will first convert all elements into strings and then use native (therefore faster) string comparison.

To make this work, we just have to override the toString method of the Object prototype to return the key.

var save = Object.prototype.toString;
Object.prototype.toString = function () { return this.key; };
 
data.sort();
 
Object.prototype.toString = save;

data.sort();

Object.prototype.toString = save;

You have to make sure that the key variable is a string. In my application, the key range is [0, 100] so the it is written as String.fromCharCode(key). If you have to deal with larger key range, the best solution is to convert the number into base 256. Make sure the number is padded with 0 because of the string comparison.

I made a little benchmark of the implementation to see how well it performs

.sort_results { border-collapse: collapse; margin-left: 2px; margin-bottom: 0; }
.sort_results td { text-align: center; width: 100px; border: 1px solid #ccc; padding: 0 5px; }
.sort_results td.right { text-align: right; width: 200px; }

toString Sort Benchmark Firefox
3.5.2
IE
8
Safari
4.528
Chrome
3.0.197
Normal – 10 000 135ms 188ms 45ms 16ms
Fast – 10 000 10ms 31ms 14ms 68ms
Improvement – 10 000 x13.5 x6.1 x3.2 /4.3
Normal – 100 000 695ms 2125ms 200ms 128ms
Fast – 100 000 101ms 437ms 46ms 326ms
Improvement – 100 000 x6.9 x4.9 x4.3 /2.5
Normal – 1 000 000 10102ms * 2736ms 970ms
Fast – 1 000 000 1158ms 6828ms 482ms 2593ms
Improvement – 1 000 000 x8.7 x5.7 /2.7

*: Script time limit has been exceeded

It gives about a 5x increase of all the browsers I have tested with except in Chrome with a 3x decrease.

Since Chrome is already times faster than all the browsers, it doesn’t look slowed by this feature. However it gives a real boost to all other browsers.

Update (24 December 2009): Chrome Array.sort() function is written directly in javascript and calls the ToString function everytime when a comparison is needed. Therefore, it is making 2 function calls (ToString(x), ToString(y) instead of one (compare(x, y)).

In order to check if that optimization will indeed give an actual boost, we can count the number of time the ToString method is being executed for 3 values. 3 times means that it is executed n time and more means that it is executed n log n times.

var need_custom_sort = (function () {
  // Fill the array with 3 values
  var array = new Array(3);
  for (var i = 0; i < 3; ++i) {
    array[i] = new Object();
  }
 
  // Override the toString method that counts how many times it is being called
  var count = 0;
  var save = Object.prototype.toString;
  Object.prototype.toString = function () { count += 1; return ""; };
 
  // Sort
  array.sort();
  Object.prototype.toString = save;
 
  // 3 times is good, more is bad!
  return (count === 3);
}());

// Override the toString method that counts how many times it is being called
var count = 0;
var save = Object.prototype.toString;
Object.prototype.toString = function () { count += 1; return ""; };

// Sort
array.sort();
Object.prototype.toString = save;

// 3 times is good, more is bad!
return (count === 3);
}());

SmallHash encodes any range of integers into the smallest possible string. This way, you can use the hash part of your url with efficiency.

You can view the source at the SmallHash Github Repository.

My problem is having these options stored in the minimum characters as possible.

  • Faction: Alliance, Horde
  • Region: US, Europe
  • Type: PvE, PvP, RP
  • Lang: EN, FR, ES, DE, RU

The two faction and region could be stored in base 2 with no problem. However, if we wanted to store the others in base 2, there would have been space left. So i started digging up into the base conversion.

Here is the code to do a base2 to base10 conversion.

base10 = 0
foreach (bit in base2) {
  base10 *= 2
  base10 += bit
}

As you can see, we multiply the final number by 2, which is the number of possibilities. So, instead of multiplying by 2, we multiply by the number of possible options and it works! The decoding process is using the same technique by changing the divisor.

To get back to our example. [Alliance, US, PvP, DE] can be expressed as [0,0,1,3] over [2,2,3,5]. It will be encoded and decoded easily with the SmallHash library:

var input = [0,0,1,3];
var encoded = SmallHash.encode(input, [2,2,3,5], 'abcdefghijklmnopqrstuvwxyz');
var decoded = SmallHash.decode(encoded, [2,2,3,5], 'abcdefghijklmnopqrstuvwxyz');
console.log(input, encoded, decoded);
// Result: [0, 0, 1, 3], "bo", [0, 0, 1, 3]

As you can see, it fits into 2 characters instead of 4 with the easy way. The gain increases with the number of data you have to encode. This can also be improved by enlarging the base characters (uppercase letter, digits and special characters).

The algorithm is fairly easy, it is the same one explain before but using the range instead of 2 (when converting in base 2). This is the pseudo-code version.

 
SmallHash = {
  // encode( [2, 4], [10, 15], '0123456789' ) : '42'
  encode: function (input, ranges, base) {
    var result = 0
    for offset = ranges.length - 1 downto 0
      result = result * ranges[offset]
      result = result + input[offset]
 
    return int2str(result, base)
  },
 
  // decode( '42', [10, 15], '0123456789' ) : [2, 4]
  decode: function (input, ranges, base) {
    input = str2int(input, base)
    var result = []
 
    for offset = 0 to ranges - 1
      result[offset] = inputs % ranges[offset]
      inputs = inputs / ranges[offset]
 
    return result;
  }
};

return int2str(result, base)
},

// decode( ’42’, [10, 15], ‘0123456789’ ) : [2, 4]
decode: function (input, ranges, base) {
input = str2int(input, base)
var result = []

for offset = 0 to ranges – 1
result[offset] = inputs % ranges[offset]
inputs = inputs / ranges[offset]

return result;
}
};

Here is the full source code. This is the same code but being less readable due to the use of BigInt and the need of managing the allocation size.

// Requires BigInt.js ( https://blog.vjeux.com/wp-content/uploads/2009/08/BigInt.js )
SmallHash = {
  encode: function (input, ranges, base) {
    // Rough majoration of the final result size
    // It makes the sum of all the minimum of bits required for each range
    var size = 0;
    for (var i = 0, len = ranges.length; i < len; i = i + 1) {
      size += Math.ceil(Math.log(ranges[i]) / Math.LN2);
    }
    var result = bigInt.int2bigInt(0, size);
    for (var bit = ranges.length - 1, pos = 0; bit >= 0; bit = bit - 1, pos = pos + 1) {
      // If the value is higher than the expected range, the value is maximized
      // Therefore the result is always valid, even if the input is not
      var parsed_bit = Math.min(Math.abs(Math.floor(input[bit])), ranges[bit] - 1);
      bigInt.mult_(result, bigInt.int2bigInt(ranges[bit], 32));
      bigInt.add_(result, bigInt.int2bigInt(parsed_bit, 32));
    }
    return bigInt.bigInt2str(result, base.length, base);
  },
  decode: function (input, ranges, base) {
    input = bigInt.str2bigInt(input, base.length, base);
    var remainder = bigInt.dup(input); // Allocates enough room for the remainder
    var result = [];
    for (var pos = 0, len = ranges.length; pos < len; pos = pos + 1) {
      bigInt.divide_(input, bigInt.int2bigInt(ranges[pos], 32), input, remainder);
      result[pos] = Number(bigInt.bigInt2str(remainder, 10, '0123456789'));
    }
    return result;
  }
};

This script is using the BigInt library from Leemon Baird. I made some changes in order not to pollute the global namespace and added the possibility to modify the base string.

Update January 2010 – SmallHash is now being used on production at wowtal.com and you can download the source at http://static.mmo-champion.com/db/js/smallhash.js.

Protected: React performance

This content is password protected. To view it please enter your password below: