Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

288CHAPTER 8

Performance

8.2.2Using the Venkman profiler

The Mozilla family of browsers enjoys a rich set of plug-in extensions. One of the older, more established ones is the Venkman debugger, which can be used to step through JavaScript code line by line. We discuss Venkman’s debugging features in appendix A. For now, though, let’s look at one of its lesser-known capabilities, as a code profiler.

To profile code in Venkman, simply open the page that you’re interested in, and then open the debugger from the browser’s Tools menu. (This assumes that you have the Venkman extension installed. If you don’t yet, see appendix A.) On the toolbar there is a clock button labeled Profile (figure 8.3). Clicking this button adds a green tick to the icon.

Venkman is now meticulously recording all that goes on in the JavaScript engine of your browser, so drag the mouse around the mousemat area for a few seconds, and then click the Profile button in the debugger again to stop profiling. From the debugger Window menu, select the Profile > Save Profile Data As option. Data can be saved in a number of formats, including CSV (for spreadsheets), an HTML report, or an XML file.

Figure 8.3 Venkman debugger for Mozilla with the Profile button checked, indicating that time spent executing all loaded scripts (as shown in the panel on the top left) is being recorded.

JavaScript execution speed

289

 

 

Figure 8.4 Fragment of the HTML profile report generated by Venkman showing the number of calls and total, minimum, maximum, and average time for each method that listens to the mouse movements over the mousemat DOM element in our example page.

Unfortunately, Venkman tends to generate rather too much data and lists various chrome:// URLs first. These are internal parts of the browser or plug-ins that are implemented in JavaScript, and we can ignore them. In addition to the main methods of the HTML page, all functions in all JavaScript libraries that we are using—including the stopwatch.js profiler that we developed in the previous sec- tion—have been recorded. Figure 8.4 shows the relevant section of the HTML report for the main HTML page.

Venkman generates results that broadly agree with the timings of our own stopwatch object—rewriting the status bar takes roughly one third as long as updating the thumbnail element.

Venkman is a useful profiling tool and it can generate a lot of data without us having to modify our code at all. If you need to profile code running across different browsers, then our stopwatch library can help you out. In the following section, we’ll look at a few example pieces of code that demonstrate some refactorings that can be applied to code to help speed it up. We’ll make use of our stopwatch library to measure the benefits.

8.2.3Optimizing execution speed for Ajax

Optimization of code is a black art. Programming JavaScript for web browsers is often a hit-or-miss affair. It stands to reason, therefore, that optimizing Ajax code is a decidedly murky topic. A substantial body of folklore surrounds this topic, and much of what is said is good. With the profiling library that we developed in section 8.2.1, however, we can put our skeptic’s hat on and put the folklore to the test. In this section, we’ll look at three common strategies for improving execution speed and see how they bear out in practice.

290CHAPTER 8

Performance

Optimizing a for loop

The first example that we’ll look at is a fairly common programming mistake. It isn’t limited to JavaScript but is certainly easy to make when writing Ajax code. Our example calculation does a long, pointless calculation, simply to take up sufficient time for us to measure a real difference. The calculation that we have chosen here is the Fibonacci sequence, in which each successive number is the sum of the previous two numbers. If we start off the sequence with two 1s, for example, we get

1, 1, 2, 3, 5, 8, ...

Our JavaScript calculation of the Fibonacci sequence is as follows:

function fibonacci(count){ var a=1;

var b=1;

for(var i=0;i<count;i++){ var total=a+b;

a=b;

b=total;

}

return b;

}

Our only interest in the sequence is that it takes a little while to compute. Now, let’s suppose that we want to calculate all the Fibonacci sequence values from 1 to n and add them together. Here’s a bit of code to do that:

var total=0;

for (var i=0;i<fibonacci(count);i++){ total+=i;

}

This is a pointless calculation to make, by the way, but in real-world programs you’ll frequently come across a similar situation, in which you need to check a value that is hard to compute within each iteration of a loop. The code above is inefficient, because it computes fibonacci(count) with each iteration, despite the fact that the value will be the same every time. The syntax of the for loop makes it less than obvious, allowing this type of error to slip into code all too easily. We could rewrite the code to calculate fibonacci() only once:

var total=0;

var loopCounter=fibonacci(count); for (var i=0;i<loopCounter;i++){

total+=i;

}

JavaScript execution speed

291

 

 

So, we’ve optimized our code. But by how much? If this is part of a large complex body of code, we need to know whether our efforts have been worthwhile. To find out, we can include both versions of the code in a web page along with our profiling library and attach a stopwatch to each function. Listing 8.5 shows how this is done.

Listing 8.5 Profiling a for loop

<html>

<head>

<link rel='stylesheet' type='text/css' href='mousemat.css' /> <link rel='stylesheet' type='text/css' href='objviewer.css' /> <script type='text/javascript' src='x/x_core.js'></script> <script type='text/javascript' src='extras-array.js'></script> <script type='text/javascript' src='styling.js'></script> <script type='text/javascript' src='objviewer.js'></script> <script type='text/javascript' src='stopwatch.js'></script> <script type='text/javascript' src='eventRouter.js'></script> <script type='text/javascript'>

function slowLoop(count){

var watch=stopwatch.getWatch("slow loop",true);

var total=0;

 

Recompute loop counter every time

for (var i=0;i<fibonacci(count);i++){

 

 

total+=i;

 

 

}

 

 

watch.stop();

 

 

alert(total);

 

 

}

 

 

function fastLoop(count){

var watch=stopwatch.getWatch("fast loop",true);

var total=0;

 

 

 

Compute loop counter once only

var loopCounter=fibonacci(count);

 

 

for (var i=0;i<loopCounter;i++){

 

 

total+=i;

 

 

 

 

}

 

 

 

 

watch.stop();

 

 

 

 

alert(total);

 

 

 

 

}

 

 

 

 

function fibonacci(count){

 

Compute Fibonacci sequence

 

var a=1;

 

 

 

 

var b=1;

 

 

 

 

for(var i=0;i<count;i++){

 

 

 

 

var total=a+b; a=b;

b=total;

}

292CHAPTER 8

Performance

return b;

}

function go(isFast){

var count=parseInt(document.getElementById("count").value); if (count==NaN){

alert("please enter a valid number"); }else if (isFast){

fastLoop(count);

}else{

slowLoop(count);

}

}

</script>

</head>

<body>

<div>

<a href='javascript:stopwatch.report("profiler")'>profile</a>  <input id='count' value='25'/> 

<a href='javascript:go(true)'>fast loop</a>  <a href='javascript:go(false)'>slow loop</a> </div>

<div>

<div class='profiler objViewBorder' id='profiler'></div> </div>

</body>

</html>

The functions slowLoop() and fastLoop() present our two versions of the algorithm and are wrapped by the go() function, which will invoke one or the other with a given counter value. The page provides hyperlinks to execute each version of the loop, passing in a counter value from an adjacent HTML forms textbox. We found a value of 25 to give a reasonable computation time on our testing machine. A third hyperlink will render the profiling report. Table 8.1 shows the results of a simple test.

Table 8.1 Profiling results for loop optimization

Algorithm

Execution Time (ms)

 

 

Original

3085

 

 

Optimized

450

 

 

JavaScript execution speed

293

 

 

From this, we can see that taking the lengthy calculation out of the for loop really does have an impact in this case. Of course, in your own code, it might not. If in doubt, profile it!

The next example looks at an Ajax-specific issue: the creation of DOM nodes.

Attaching DOM nodes to a document

To render something in a browser window using Ajax, we generally create DOM nodes and then append them to the document tree, either to document.body or to some other node hanging off it. As soon as it makes contact with the document, a DOM node will render. There is no way of suppressing this feature.

Re-rendering the document in the browser window requires various layout parameters to be recalculated and is potentially expensive. If we are assembling a complex user interface, it therefore makes sense to create all the nodes and add them to each other and then add the assembled structure to the document. This way, the page layout process occurs once. Let’s look at a simple example of creating a container element in which we randomly place lots of little DOM nodes. In our description of this example, we referred to the container node first, so it seems natural to create that first. Here’s a first cut at this code:

var container=document.createElement("div"); container.className='mousemat';

var outermost=document.getElementById('top'); outermost.appendChild(container);

for(var i=0;i<count;i++){

var node=document.createElement('div'); node.className='cursor'; node.style.position='absolute'; node.style.left=(4+parseInt(Math.random()*492))+"px"; node.style.top=(4+parseInt(Math.random()*492))+"px"; container.appendChild(node);

}

The element outermost is an existing DOM element, to which we attach our container, and the little nodes inside that. Because we append the container first and then fill it up, we are going to modify the entire document count+1 times! A quick bit of reworking can correct this for us:

var container=document.createElement("div"); container.className='mousemat';

var outermost=document.getElementById('top'); for(var i=0;i<count;i++){

var node=document.createElement('div'); node.className='cursor'; node.style.position='absolute'; node.style.left=(4+parseInt(Math.random()*492))+"px";

294CHAPTER 8

Performance

node.style.top=(4+parseInt(Math.random()*492))+"px";

container.appendChild(node);

}

outermost.appendChild(container);

In fact, we had to move only one line of code to reduce this to a single modification of the existing document. Listing 8.6 shows the full code for a test page that compares these two versions of the function using our stopwatch library.

Listing 8.6 Profiling DOM node creation

<html>

<head>

<link rel='stylesheet' type='text/css' href='mousemat.css' /> <link rel='stylesheet' type='text/css' href='objviewer.css' /> <script type='text/javascript' src='x/x_core.js'></script> <script type='text/javascript' src='extras-array.js'></script> <script type='text/javascript' src='styling.js'></script> <script type='text/javascript' src='objviewer.js'></script> <script type='text/javascript' src='stopwatch.js'></script> <script type='text/javascript' src='eventRouter.js'></script> <script type='text/javascript'>

var cursor=null;

function slowNodes(count){

var watch=stopwatch.getWatch("slow nodes",true); var container=document.createElement("div"); container.className='mousemat';

var outermost=document.getElementById('top'); outermost.appendChild(container); Append empty container at start for(var i=0;i<count;i++){

var node=document.createElement('div'); node.className='cursor'; node.style.position='absolute'; node.style.left=(4+parseInt(Math.random()*492))+"px"; node.style.top=(4+parseInt(Math.random()*492))+"px"; container.appendChild(node);

}

watch.stop();

}

function fastNodes(count){

var watch=stopwatch.getWatch("fast nodes",true); var container=document.createElement("div"); container.className='mousemat';

var outermost=document.getElementById('top'); for(var i=0;i<count;i++){

var node=document.createElement('div');

JavaScript execution speed

295

 

 

node.className='cursor';

node.style.position='absolute';

node.style.left=(4+parseInt(Math.random()*492))+"px";

node.style.top=(4+parseInt(Math.random()*492))+"px";

container.appendChild(node);

}

 

Append full container at end

outermost.appendChild(container);

 

 

watch.stop();

 

 

}

 

 

function go(isFast){

var count=parseInt(document.getElementById("count").value); if (count==NaN){

alert("please enter a valid number"); }else if (isFast){

fastNodes(count);

}else{

slowNodes(count);

}

}

</script>

</head>

<body>

<div>

<a href='javascript:stopwatch.report("profiler")'>profile</a>  <input id='count' value='640'/> 

<a href='javascript:go(true)'>fast loop</a>  <a href='javascript:go(false)'>slow loop</a> </div>

<div id='top'>

<div class='mousemat' id='mousemat'></div>

<div class='profiler objViewBorder' id='profiler'></div> </div>

</body>

</html>

Again, we have a hyperlink to invoke both the fast and the slow function, using the value in an HTML form field as the argument. In this case, it specifies how many little DOM nodes to add to the container. We found 640 to be a reasonable value. The results of a simple test are presented in table 8.2.

296CHAPTER 8

Performance

Table 8.2 Profiling results for DOM node creation

Algorithm

Number of Page Layouts

Execution Time (ms)

 

 

 

Original

641

681

 

 

 

Optimized

1

461

 

 

 

Again, the optimization based on received wisdom does make a difference. With our profiler, we can see how much of a difference it is making. In this particular case, we took almost one third off the execution time. In a different layout, with different types of nodes, the numbers may differ. (Note that our example used only absolutely positioned nodes, which require less work by the layout engine.) The profiler is easy to insert into your code, in order to find out.

Our final example looks at a JavaScript language feature and undertakes a comparison between different subsystems to find the bottleneck.

Minimizing dot notation

In JavaScript, as with many languages, we can refer to variables deep in a complex hierarchy of objects by “joining the dots.” For example:

myGrandFather.clock.hands.minute

refers to the minute hand of my grandfather’s clock. Let’s say we want to refer to all three hands on the clock. We could write

var hourHand=myGrandFather.clock.hands.hour; var minuteHand=myGrandFather.clock.hands.minute;

var secondHand=myGrandFather.clock.hands.second;

Every time the interpreter encounters a dot character, it will look up the child variable against the parent. In total here, we have made nine such lookups, many of which are repeats. Let’s rewrite the example:

var hands=myGrandFather.clock.hands; var hourHand=hands.hour;

var minuteHand=hands.minute; var secondHand=hands.second;

Now we have only five lookups being made, saving the interpreter from a bit of repetitive work. In a compiled language such as Java or C#, the compiler will often optimize these repetitions automatically for us. I don’t know whether JavaScript interpreters can do this (and on which browsers), but I can use the stopwatch library to find out if I ought to be worrying about it.

JavaScript execution speed

297

 

 

The example program for this section computes the gravitational attraction between two bodies, called earth and moon. Each body is assigned a number of physical properties such as mass, position, velocity, and acceleration, from which the gravitational forces can be calculated. To give our dot notation a good testing, these properties are stored as a complex object graph, like so:

var earth={ physics:{

mass:10,

pos:{ x:250,y:250 }, vel:{ x:0, y:0 }, acc:{ x:0, y:0 }

}

};

The top-level object, physics, is arguably unnecessary, but it will serve to increase the number of dots to resolve.

The application runs in two stages. First, it computes a simulation for a given number of timesteps, calculating distances, gravitational forces, accelerations, and other such things that we haven’t looked at since high school. It stores the position data at each timestep in an array, along with a running estimate of the minimum and maximum positions of either body.

In the second phase, we use this data to plot the trajectories of the two bodies using DOM nodes, taking the minimum and maximum data to scale the canvas appropriately. In a real application, it would probably be more common to plot the data as the simulation progresses, but I’ve separated the two here to allow the calculation phase and rendering phase to be profiled separately.

Once again, we define two versions of the code, an inefficient one and an optimized one. In the inefficient code, we’ve gone out of our way to use as many dots as possible. Here’s a section (don’t worry too much about what the equations mean!):

var grav=(earth.physics.mass*moon.physics.mass) /(dist*dist*gravF);

var xGrav=grav*(distX/dist); var yGrav=grav*(distY/dist);

moon.physics.acc.x=-xGrav/(moon.physics.mass); moon.physics.acc.y=-yGrav/(moon.physics.mass); moon.physics.vel.x+=moon.physics.acc.x; moon.physics.vel.y+=moon.physics.acc.y; moon.physics.pos.x+=moon.physics.vel.x; moon.physics.pos.y+=moon.physics.vel.y;

298CHAPTER 8

Performance

This is something of a caricature—we’ve deliberately used as many deep references down the object graphs as possible, making for verbose and slow code. There is certainly plenty of room for improvement! Here’s the same code from the optimized version:

var mp=moon.physics; var mpa=mp.acc;

var mpv=mp.vel; var mpp=mp.pos; var mpm=mp.mass;

...

var grav=(epm*mpm)/(dist*dist*gravF); var xGrav=grav*(distX/dist);

var yGrav=grav*(distY/dist);

mpa.x=-xGrav/(mpm); mpa.y=-yGrav/(mpm); mpv.x+=mpa.x; mpv.y+=mpa.y; mpp.x+=mpv.x; mpp.y+=mpv.y;

We’ve simply resolved all the necessary references at the start of the calculation as local variables. This makes the code more readable and, more important, reduces the work that the interpreter needs to do. Listing 8.7 shows the code for the complete web page that allows the two algorithms to be profiled side by side.

Listing 8.7 Profiling variable resolution

<html>

<head>

<link rel='stylesheet' type='text/css' href='mousemat.css' /> <link rel='stylesheet' type='text/css' href='objviewer.css' /> <script type='text/javascript' src='x/x_core.js'></script> <script type='text/javascript' src='extras-array.js'></script> <script type='text/javascript' src='styling.js'></script> <script type='text/javascript' src='objviewer.js'></script> <script type='text/javascript' src='stopwatch.js'></script> <script type='text/javascript' src='eventRouter.js'></script> <script type='text/javascript'>

var moon={

 

Initialize planetary bodies

 

physics:{

 

 

mass:1,

 

 

pos:{

x:120,y:80 },

vel:{

x:-24, y:420 },

Initialize planetary bodies

JavaScript execution speed

299

 

 

acc:{ x:0, y:0 }

}

};

var earth={ physics:{ mass:10,

pos:{ x:250,y:250 }, vel:{ x:0, y:0 }, acc:{ x:0, y:0 }

}

};

var gravF=100000;

function showOrbit(count,isFast){

var data=(isFast)

?

 

b Select

 

fastData(count)

:

 

calculation

slowData(count);

 

type

var watch=stopwatch.getWatch("render",true);

var canvas=document.

c Render orbit

getElementById('canvas');

 

var dx=data.max.x-data.min.x;

 

var dy=data.max.y-data.min.y;

 

var sx=(dx==0) ? 1

: 500/dx;

 

var sy=(dy==0) ? 1

: 500/dy;

 

var offx=data.min.x*sx; var offy=data.min.y*sy;

for (var i=0;i<data.path.length;i+=10){ var datum=data.path[i];

var dpm=datum.moon; var dpe=datum.earth;

var moonDiv=document.createElement("div"); moonDiv.className='cursor'; moonDiv.style.position='absolute'; moonDiv.style.left=parseInt((dpm.x*sx)-offx)+"px"; moonDiv.style.top=parseInt((dpm.x*sx)-offy)+"px"; canvas.appendChild(moonDiv);

var earthDiv=document.createElement("div"); earthDiv.className='cursor'; earthDiv.style.position='absolute'; earthDiv.style.left=parseInt((dpe.x*sx)-offx)+"px"; earthDiv.style.top=parseInt((dpe.x*sx)-offy)+"px"; canvas.appendChild(earthDiv);

}

watch.stop();

}

function slowData(count){ d Use dot notation a lot var watch=stopwatch.getWatch("slow orbit",true);

300

CHAPTER 8

 

 

Performance

 

 

var data={

 

 

min:{x:0,y:0},

 

 

max:{x:0,y:0},

 

 

path:[]

 

 

};

 

 

...

 

 

}

 

 

watch.stop();

 

 

return data;

 

 

}

 

 

function fastData(count){

e Use dot notation sparingly

var watch=stopwatch.getWatch("fast orbit",true); var data={

min:{x:0,y:0},

max:{x:0,y:0},

path:[]

};

...

}

watch.stop(); return data;

}

function go(isFast){

var count=parseInt(document.getElementById("count").value); if (count==NaN){

alert("please enter a valid number"); }else{

showOrbit(count,isFast);

}

}

</script>

</head>

<body>

<div>

<a href='javascript:stopwatch.report("profiler")'>profile</a>  <input id='count' value='640'/> 

<a href='javascript:go(true)'>fast loop</a>  <a href='javascript:go(false)'>slow loop</a> </div>

<div id='top'>

<div class='mousemat' id='canvas'> </div>

<div class='profiler objViewBorder' id='profiler'></div> </div>

JavaScript execution speed

301

 

 

</body>

</html>

The structure should be broadly familiar by now. The functions slowData() d and fastData() econtain the two versions of our calculation phase, which generates the data structures b. I’ve omitted the full algorithms from the listing here, as they take up a lot of space. The differences in style are described in the snippets we presented earlier and the full listings are available in the downloadable sample code that accompanies the book. Each calculation function has a stopWatch object assigned to it, profiling the entire calculation step. These functions are called by the showOrbit() function, which takes the data and then creates a DOM representation of the calculated trajectories c. This has also been profiled by a third stopwatch.

The user interface elements are the same as for the previous two examples, with hyperlinks to run the fast and the slow calculations, passing in the text input box value as a parameter. In this case, it indicates the number of timesteps for which to run the simulation. A third hyperlink displays the profile data. Table 8.3 shows the results from a simple run of the default 640 iterations.

Table 8.3 Profiling results for variable resolution

Algorithm

Execution Time (ms)

 

 

Original calculation

94

 

 

Optimized calculation

57

 

 

Rendering (average)

702

 

 

Once again, we can see that the optimizations yield a significant increase, knocking more than one-third from the execution time. We can conclude that the folk wisdom regarding variable resolution and the use of too many dots is correct. It’s reassuring to have checked it out for ourselves.

However, when we look at the entire pipeline of calculation and rendering, the optimization takes 760 ms, as opposed to the original’s 796 ms—a savings closer to 5 percent than 40 percent. The rendering subsystem, not the calculation subsystem, is the bottleneck in the application, and we can conclude that, in this case, optimizing the calculation code is not going to yield great returns.

This demonstrates the broader value of profiling your code. It is one thing to know that a piece of code can be optimized in a particular way and another to