February 17, 2010

The Art (and Joys) of Profiling... a real-world example

This article will outline my experiences profiling a Javascript table-sorter widget that I recently developed.

Profiling an application takes practice and a little dedication—but it can be fun, too!  I recently had a wonderful time profiling a new Javascript widget for dynamically sorting HTML tables. I will use my recent experience to provide a few tips to novice profilers (if there be any).

We're running out of plugins here!
The other day, I hunted around for a jQuery plugin to sort raw <table> rows that required a minimum level of code to enable it. I was looking for something that could automatically find HTML tables, detect how to sort the columns, and that had lots of default options.

I found one such plugin here: the jQuery Tablesorter plugin. It works very fast, even on large data-sets; it even has full-text filtering and paging add-ons. Unfortunately, I ran into problems: there are a few usability issues (particularly with the pager plugin) and a lack of real programmatic access to the data. For example, I wasn't able to append or delete rows to the table because the pager didn't recognize them and would immediately discard them when changing the page or changing the sorting column... A new jQuery plugin was definitely called for!!!

Taking a whack at it...
As an intermediate Javascript developer, half of my goal was to see if I could get something to work. Another half of my goal was to make something worthy of re-distribution (to help "get my name out there"). And another half of my goal was to correct the lack of table-sorting plugins (to make the world a better place).

My first step was to take a simple <table> and decorating it with CSS classes so that it looks pretty and has the little "sort-me" images beside the column headers. With jQuery, the jQuery-UI manual, and a nice table-sorting theme to be found here, this was pretty straight-forward. I then added some "click" listeners to the column headers that determined which column should be sorted, and in which direction by looking at the current active/direction classes assigned to the headers (not a big deal for jQuery).

The next big step was to sort the data. I referred back to the original table-sorter plugin for guidance. I saw that they interacted solely with a cache of the original table rows. I ported the cache-construction code. They did some funky eval'ing to create a multi-column sorter, so I wrote my own sort-by-column function (as a call-back to Arrays.sort). I used jQuery's built-in functionality to remove and append table-body rows, like so:

//removes all "tr" children from the tbody DOM element
jQuery(tbody).children().remove(); //I hadn't found jQuery(...).empty(), yet.
...
//appends a single "tr" row to the tbody
jQuery(tbody).append(jQuery(some_html)); //repeated over, and over
...

I fired up the web-browser and tried it out. It worked! I hastily added functionality to programmatically sort, re-sort, filter, and reset the data. Things were looking mighty fine! I noticed that table-sorter had an example with about 1000 rows, just to show off their speed. I decided that was just the thing to test out the budding plugin, so I copied their data (1023 rows, 7 columns) and attached my sorter...

The bad and the ugly... (Step 1 profiling)
On Firefox 3.5 (and a dual-core Centrino), it took about 7 seconds of a frozen browser (I counted) before the data was initially sorted. Maybe a little better to change sort columns, but it still froze the browser. What to do? :(

I'm not familiar with any Javascript profiling tools (I'm sure they're out there), but I am familiar with "alert()"... the poor man's profiler. (Or you could use console.log). I added some benchmarking statements, shamelessly copied from the other plugin's benchmark function: (dual-licensed under MIT and GPL)

//give it a message (String) and a starting time (Date), it will compute and display the time delta

function benchmark(message, startTime) {
    alert(message + ": " + (new Date().getTime() - startTime.getTime()) + "ms"); //or you could log it
}

//used like this:

var startDate = new Date();

... run some code

benchmark("Time to construct my table-sorter", startDate);


To get a better understanding of what was taking so long, I put benchmarking statements around the major methods: building the cache, sorting, replacing the table rows, colourizing the rows (striping), and "activating" the sorted column(s). I got inital numbers something like this:
  • building cache: ~230ms
  • sorting:  2ms
  • replacing rows: ~6200ms
  • colourizing rows: ~180ms
  • activating rows: ~370ms
  • Total time: approx: 7200ms (I skipped other trivial steps).
I was absolutely amazed that it took only 2ms to do the sorting. I was appalled that it took more than 6 seconds to replace some table rows. So, I broke that step down further:
  • removing table rows: 5469ms
  • build+append rows: 896ms
Very interesting results... one jQuery method ($tbody.children().remove()) was taking 75% of the time!

A Better Mouse-trap... (Step 2 profiling)
I did a little bit more homework. I happened upon this wonderful live demo of various ways to append/delete table rows. I found that the DOM interface tbody.removeChild(tbody.firstChild) method worked very quickly. It dropped my time to deconstruct table rows down to ~180ms! We were back in business!

I tried a number of other ideas to speed-up the slowest parts of the code:
  • By concatenating a string-representation of all the table rows and setting the innerHTML property on the tbody, the time to build+append rows dropped from 900ms to 230ms.
  • To remove old row, setting the innerHTML of tbody to empty-string took only 90ms.
  • By iterating over the table rows and using DOM manipulation, dropped the time to activate the sort column from 370ms down to around 100ms (meaning, I add a decorator class to the corresponding cell of each tr).
  • By removing a ":visible" filter in a jQuery chain, I was able to remove 100ms from the time it took to apply row-striping (horizontal zebra-striping).

Lessons to be learned:
With only a small amount of work and some attention to detail, I was able to drop a 7-second script down to around 0.8 seconds. If I had tried to pre-optimize any part of the script, it probably would have been the wrong part (most likely my sorting implementation—which turned out to be fastest code in the whole plugin!).

Instead, I was able to quickly get a working prototype off the ground using jQuery and then—with guidance from various profiling runs—focus my efforts on what needed the most help (it happened to be my over-exuberance for jQuery). As a result, I found that explicitly interating over the <tbody> DOM was light-years faster than the (un-optimized) jQuery calls.

But there were cross-browser complications and further improvements to be made... So stay tuned!

The author is growing rather fond of Javascript, thanks in large part to jQuery. The more he gets to know Javascript, the more he wants to write in Lisp.

February 16, 2010

Announcing...

Natural Parenting
xkcd.com/674/

Turn on mod_deflate!

This article is a quick bit of advice for those developer-turned-web-admins out there.
This article is specifically about the Apache "httpd" web-server, but the same advice applies to other web-servers.

While looking into Javascript compressors (minfiers and packers), I bumped into HTTP compression. I knew about HTTP compression, but it wasn't something I'd really thought about.

After a quick read-up at Wikipedia and after scanning a few HOW-TOs, I checked out the web-server that I maintain at my place of work. What do you know, but it (Ubuntu 9.x + Apache2.2) already came with mod_deflate.c enabled and marginally configured.

According to Danny Vogler's advice, I changed the following:

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml
</IfModule>

... into this: (modifications in red)

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/javascript application/x-javascript text/css
</IfModule>

The web-server will now attempt to compress Javascript and CSS content-types—if the client's web-browser supports it, of course.


I then turned my browser to an internal reports page, one that uses jQuery, jQuery-UI, and a (custom) jQuery-UI theme. Counting the site's own CSS theming (including media=print stylesheet), there are four stylesheets and four javascript files attached.

With text/html compression already turned on, the whole mess of content came out to 222,745 Bytes (around 218 KB), including pictures. With text/css and application/javascript content-types being compressed, the content came down to 69043 Bytes (67 KB)—only 30% of the original size! ... Yep, it made my day.