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).
- removing table rows: 5469ms
- build+append rows: 896ms
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.