JavaScript Function Call Profiling

With jQuery 1.3.2 out the door I’ve been looking for more ways to profile and optimize jQuery.

Previously I did a survey of jQuery-using sites to figure out which selectors they were using. This led to the construction of the new Sizzle Selector Engine which targeted those selectors for improvement.

Additionally, I constructed a deep profiling plugin for jQuery which helped to spot methods that were taking a long time to run in live jQuery sites. This helped bring about the improvements in jQuery 1.2.6, 1.3, and 1.3.2.

What do we tackle next? A good place to start would be to tackle optimizing methods that are obviously inefficient – but how do we determine that? One way would be to measure the number of function calls that occur every time a method is run. Firebug provides this information in its profiling data (along with how long it takes to run each method). Unfortunately it’s very clunky to manually type out code, check the results in the console, and determine if they’re bad or if they’ve changed. If only there was a way to progamatically get at those numbers.

FireUnit Profiling Methods

Yesterday I did some work to make getting at the profiling data possible, adding two new methods to FireUnit.

fireunit.getProfile();

Run this method after you’ve run console.profile(); and console.profileEnd(); to get a full dump of the profiling information. For example, given the following profile run:

You’ll get the following JavaScript object returned from fireunit.getProfile():

The second method added to FireUnit provides an easy way to execute and profile a single function. Roughly, this method starts the profiler, executes the function, stops the profiler, and then returns the results from getProfile(). Additionally, it watches for any exceptions that might be thrown and makes sure that the profiler is cleanly turned off anyway (a frequent frustration of mine).

That the ‘extensions.firebug.throttleMessages’ property in ‘about:config’ is set to ‘false’.

The Results

I put up a test page so that I could quickly run through some jQuery methods to see how they stacked up.

Here are the results of running against jQuery 1.3.2 (“Method” is the jQuery method that was called, with the specified arguments, “Calls” is the number of function calls that occurred when executing the method, “Big-O” is a very rough Big-O Notation for the function calls):

Method

Calls

Big-O

.addClass(“test”);

542

6n

.addClass(“test”);

592

6n

.removeClass(“test”);

754

8n

.removeClass(“test”);

610

6n

.css(“color”, “red”);

495

5n

.css({color: “red”, border: “1px solid red”});

887

9n

.remove();

23772

2n+n2

.append(“<p>test</p>”);

307

3n

.append(“<p>test</p><p>test</p><p>test</p><p>test</p><p>test</p>”);

319

3n

.show();

394

4n

.hide();

394

4n

.html(“<p>test</p>”);

28759

3n+n2

.empty();

28452

3n+n2

.is(“div”);

110

.filter(“div”);

216

2n

.find(“div”);

1564

16n

We can immediately see, by looking at the big-O notation, that most jQuery methods execute at least one function for every element that they have to operate against. addClass runs about 6 functions per element, filter runs about 2, and ‘is’ runs only 1.

We can see the problematic functions sticking out like a massive sore thumb: .remove(), .empty(), and .html() – they all run over n2 function calls, which is a huge issue. (These numbers are all large for a simple reason: .html() uses .empty(), .empty() uses .remove(), and .remove() is obviously inefficient.) While function calls do not, necessarily, indicate slow code (a lot of jQuery’s internal functions are pretty lightweight) it is very likely to indicate inefficiently-written code.

I poked around the code for a little bit and realized that .remove() could be dramatically simplified. I filed a ticket and landed a patch which resulted in these much-improved numbers:

Method

Calls

Big-O

.remove();

298

3n

.html(“<p>test</p>”);

507

5n

.empty();

200

2n

I’m really excited by this new tool. Automating the process of code profiling opens up whole avenues of exploration. Even using nothing more than the above tool I can immediately see room for improving just about every jQuery method.

It’s also be very interesting to have this running in some sort of continuous integration setting, to catch any egregious regressions – but I’ll leave that for another day.

Fascinating analysis! Profiling single functions with fireunit.profile will be a major convenience.

May I suggest you remove the “very rough” Big-O notation? Those expressions in n are useful for sure, but they aren’t really valid in the asymptotic sense: all the multiples of n are still just linear, and expressions like 2n+n^2 collapse to n^2. I’m sure you know this, and I don’t blame you at all for abusing the notation. I’m just suggesting that you’d be better off keeping the expressions as they are and dropping the Os.

@Ben: Yeah, that’s true – I just removed the O(…) bit since it’s really only in the style of big-O notation.

@Blaise: In some cases they might – but we would certainly want to avoid that. If there’s one thing that the jQuery code base is good at it’s code reuse. Unfortunately that comes at a cost, so we’ll need to decide if and when to use the best techniques for optimization vs. simplicity.

@Nosredna: Right, but in this case the number before actually matters for doing optimization – rounding it off and only giving the asymptote greatly diminishes the usefulness of the analysis. There’s a huge difference between 16n and 2n and it’s important to point that out.

Another, more substantive, thought: have you thought about letting fireunit.profile accept an optional second parameter to specify a number of times to call the function? Not sure if the results should reflect the average or the total number of functions called, in that case. (On the other hand, it would be trivial to write a wrapper function that calls the target function repeatedly.)

@Ben: I’m hesitant to do that because it’ll end up increasing the total number of function calls (whereas the profiler seems to be pretty accurate even with a single call). At least for now I think I’d recommend just doing a loop inside the function if you wish to increase repetition.

Awesome to see you tackling the performance aspect of jQuery’s internals.
May I suggest taking a look at removing .each everywhere you can internally? I know there are times when you need the closure or the ambiguous object to loop through, but most of the cases I’ve found in there seem a bit gratuitous, and the extra costs add up quite quickly compared to native loops.

Ah, I was hoping it was in firebug so I could see the big O on my projects easily. Very nice work though, I’m thoroughly impresses with the emphasis on speed for the entire library for the 1.3.1+ releases. Faster, faster, faster!

Wow, my problem is the same as Sebastian Werner while using Chinese verison of Firefox. The “ms” and “call” have been translated into Chinese in my firebug console, so the RE in fireunit.js line 391 doesn’t work properly and raises an error in line 393.

This is great work. It correlates with one of the issues we have been running into when using jquery: the remove operation is *very* expensive but really necessary when unbinding event handlers to try to avoid memory leaks. This was all the more problematic that JQuery seems to include some sort of cache which keeps a reference to the DOM elements. For large JS applications, it would be great if JQuery could evolve to allow support for more fine grain support for DOM binding and better control over the cache otherwise, it becomes very difficult for the application to proactively and aggressively break DOM/JS references and help garbage collector (at least that has been our experience).

John, I have tested the first suggestion and I obtain 6 as result.var cleanData = (function(cache){
return function(elems){
for(var i=elems.length; i; delete cache[elems[--i][expando]]);
}})(jQuery.cache);
It is quite weird because in the profiler table things are a bit different:remove() 98 51.32% 2.512ms 4.392ms 0.045ms 0.035ms 0.078ms
but the same remove is not present in every table. How should I read such result then? Thank you for the extension, it works perfectly and I guess it’s a must for performances optimizations.
Regards

John, I can confirm my code with direct delete is up to 2 times faster in every browser IE included.
In few words you do not need the if because if with an implicit cast plus hash search in the object is slower than a possibly missed delete over the same object.
(I tried to post the little test I did but for some reason it disappeared …)

Regarding local unbinding, imaging that you have a DOM subtree into which you have injected click handlers using jquery. How would you go about unbinding the event handlers from the DOM elements without doing a massive delete?