Measuring Web application response time: Meet the client

Server-side execution is only half of the story

Plenty of Web applications rely on JavaScript or some other client-side scripting, yet most developers only measure server-side execution time. Client-side execution time is just as important. In fact, if you're measuring from the end-user perspective, you should also be looking at network time. In this article Srijeeb Roy introduces a lightweight approach to capturing the end user's experience of application response time. He also shows you how to send and log client-side response times on a server for future analysis. Level: Intermediate

Capturing the overall server-side execution time of a Web request is easy to do in a Java EE application. You might write a Filter (implementing javax.servlet.Filter) to capture the request before it hits the actual Web component -- before it reaches a servlet or a JSP, for instance. When the request reaches the Filter, you store the current time; when the Filter handle returns after executing the doFilter() method, you store the current time again. Using these two timestamps, you can calculate the time it takes to process the request on the server. This gives the overall server-side execution time for each request.

Download the sample code

client-response_src.zip is the sample code package that accompanies this article. This sample code package includes two directories -- JavaScripts and JavaSource -- and a text file named web.xml.entry.txt. Inside JavaScripts, you can find two more directories, named OpenSourceXMLHttpRequestJS and timecapturejs. Inside OpenSourceXMLHttpRequestJS you can find the open source Ajax implementation JavaScript file XMLHttpRequest.src.js, which you'll learn about later in this article; inside timecapturejs, you can find a JavaScript file named client_time_capture.js, which contains all the JavaScript that I discuss here.

The JavaSource directory contains all the Java files needed for the sample app discussed in the article, arranged in a proper package structure (inside the com/tcs/tool/filter directory). Last but not least is web.xml.entry.txt, which contains the web.xml entries which you will need to include in your Web application deployment descriptor.

You can also use tools to capture the time of individual method execution. Most of these tools pump additional bytecode inside the classes, with a JAR, WAR, or EAR file; you then deploy the EAR or WAR to instrument your application. (The open source Jensor project provides excellent bytecode instrumentation for server-side code; see Resources.)

These tools are very useful for measuring server-side execution, but it is also important to capture response time from an end-user perspective. Some applications that process server-side requests very quickly still run slowly due to network bottlenecks. Convoluted JavaScript in a page's onload method can also take considerable time to execute, even after the response has traveled back to the browser. Measuring the end-user's perception of response time means accounting for the time required to do the following:

  • Execute code on the server
  • Send information over the network
  • Execute client-side JavaScript

Capturing client-side performance

It is very difficult to write a generic tool that provides client-side measurements (like those captured on the server side) without taking a performance hit in the application. A better alternative is to take a few steps to measure client-side performance while developing your application. The approach I discuss in this article will allow you to capture the actual response time experienced by the end user in most cases. This is not a generic tool that can be applied blindly; rather, it is an approach that should be applicable for many projects.

To begin, you need to understand how a request originates from the browser, traverses the network, and eventually reaches the server. Figure 1 illustrates a typical scenario.

Time captures at different points, illustrated.
Figure 1. Time captures at different points (click to enlarge)

In Figure 1 a request has been initiated from the browser at a certain time -- call it t0. It reaches the server and hits the Filter at t1. Next, the request is forwarded to a servlet and to JSPs (or perhaps to a POJO or EJB). The call then returns to the Filter and leaves it at t2. In most cases, developers calculate the response time as t2 minus t1. They log this time and use it as a basis for analysis. But the story does not actually end here.

The need for client-side measurement shows up when the response traverses back to browser. Say it reaches the browser at t3. If the Web page has an onload method in it, that needs to be executed; let's call the time at which the method ends t4. From the end user's perspective, the actual time taken by the action would be either t3 minus t0 (if the onload method is not present in the Web page) or t4 minus t0 (if the onload method is present). In this article, you'll learn how to calculate t3 minus t0 or t4 minus t0, not just t2 minus t1.

Consider a scenario where an onload method is present in the Web page. If you can capture the values of t0 and t4 in the browser, send them to the server, and then log the data, you will be able to analyze the real end-user response time. Effectively, the problem domain falls into two parts:

  • Capturing t0 and t4
  • Sending the t0 and t4 values to server

Capturing t0 and t4

You'll use a cookie to store the values of t0 and t4. Figure 2 shows how you will store t0 and t4 and send them back to the server for logging.

Capturing end-user time values, illustrated.
Figure 2. Capturing end-user time values (click to enlarge)

Capturing t0

To capture the time a request was initiated from the browser -- what we're calling t0 -- you must intercept the server-side request initiation from the browsers. A server-side request could be initiated when the user clicks a Submit button, for example, or clicks on a link, or calls JavaScript's form.submit, window.open, or window.showModalDialog methods, or even calls location.replace. However the request is initiated, you must intercept it and capture the current time before initiation.

Most of the server-side initiations mentioned -- those that replace the current page -- can be intercepted by the window.onbeforeunload event. Therefore, if you can attach an onbeforeunload event to the window object, you can capture t0 when the onbeforeunload event is fired. Take a look at the JavaScript functions in Listing 1 to see how this could work.

Listing 1. Using the onbeforeunload event to capture t0

function addOnBeforeUnloadEvent() {
   var oldOnBeforeUnload = window.onbeforeunload;
   window.onbeforeunload = function() {
      var ret;
      if ( oldOnBeforeUnload ) {
         ret = oldOnBeforeUnload();
      }
      captureTimeOnBeforeUnload();
      if ( ret ) return ret;
   }
}

function captureTimeOnBeforeUnload() {
   //capturing t0 here
   createCookie('pagepostTime', getDateString());
}

In the addOnBeforeUnloadEvent function, you are first storing the window's existing onbeforeunload event. You then override the onbeforeunload method on the window object. Here, you are actually attaching an anonymous JavaScript function. In that anonymous function, you first check to see if there is an existing onbeforeunload event already attached to the window. If there is, you call that function. Then you call captureTimeOnBeforeUnload to capture t0.

In the captureTimeOnBeforeUnload function, you have used two more JavaScript methods: getDateString and createCookie. These are shown in more detail in Listings 2 and 3.

Listing 2. getDateString

function getDateString() {
   var dt1 = new Date();
   var dtStr = dt1.getFullYear() + "/" + (dt1.getMonth() + 1)+
      "/" + dt1.getDate() + " " +  dt1.getHours() + ":" +
      dt1.getMinutes() + ":" + dt1.getSeconds()+ ":" +
      dt1.getMilliseconds();
   return dtStr;
}

The getDateString method creates an instance of the JavaScript Date object. You create the date string by calling methods like getFullYear, getMonth, and the like on the Date object.

Listing 3. createCookie

function createCookie(name, value, days) {
   var expires = "";
   if (days) {
      var date = new Date();
      date.setTime(date.getTime()+(days*24*60*60*1000));
      expires = "; expires=" + date.toString();
   }
   else expires = "";
   document.cookie = name+"="+value+expires+"; path=/";
}

Listing 3 shows you how to write a cookie using JavaScript. Please note: while creating the cookie, you are not passing any values for the days parameter. You want the cookies to only be available for a particular browser session. If the user closes the browser, all the cookies written by the instrumentation code will be removed automatically. Thus, for this instrumentation code, the days parameter has not been used.

Most server-side initiation points can be intercepted, and you can write a pagepostTime cookie that will contain the t0 value. However, a few server-side initiation points cannot be captured using onbeforeunload. For instance, the window.open method opens a new window without replacing the current page; as the parent page is not unloaded, the onbeforeunload event will not be generated. Another example is the window.showModalDialog function (which is not supported in all browsers).

Hence, for server-side initiation that does not unload the existing page, you need to consider a different approach: intercepting the window.open or window.showModalDialog functions. Look at the JavaScript code snippet in Listing 4 to see how you can override the window.open method.

Listing 4. Capturing t0 for window.open

var origWindowOpen = window.open;
window.open = captureTimeWindowOpen;

function captureTimeWindowOpen() {
   createCookie('pagepostTime', getDateString());
   if (args.length == 1) return origWindowOpen(args[0]);
   else if (args.length == 2) return origWindowOpen(args[0],args[1]);
   else if (args.length == 3) return origWindowOpen(args[0],args[1],args[2]);
}

You can probably guess from Listing 4 that you'll be storing the original window.open method in origWindowOpen. Then you override window.open with the captureTimeWindowOpen function. Inside captureTimeWindowOpen, you again create the pagepostTime cookie with the current time value, and then call the original window.open method (which earlier you stored in origWindowOpen).

Listing 5 illustrates how you can override the window.showModalDialog method.

Listing 5. Capturing t0 for window.showModalDialog

var origWindowMD = window.showModalDialog;
window.showModalDialog = captureTimeShowModalDialog;


function captureTimeShowModalDialog() {
   var args = captureTimeShowModalDialog.arguments;
   createCookie('pagepostTime', getDateString() );
   if (args.length == 1) return origWindowMD(args[0]);
   else if (args.length == 2) return origWindowMD(args[0],args[1]);
   else if (args.length == 3) return origWindowMD(args[0],args[1],args[2]);
}

Listing 5 should require no further explanation; it works in exactly the same fashion as window.showModalDialog.

Capturing t4

To capture the time at which the method ended (what we're calling t4) you will use the onload event. Keep in mind, though, that a page may not have any onload function attached to its body at all; in such a case you'd technically be measuring the time called t3 in Figures 1 and 2. For simplicity's sake, I will refer to t4 throughout. To make up for the potential lack of an onload method, you'll attach one to the window object using anonymous JavaScript. If the page already has an onload function, you'll need to make sure that the existing script executes first; only afterwards will your anonymous script execute to capture the current time. Listing 6 illustrates how to override the onload function.

Listing 6. Using the onload function to capture t4

function addLoadEvent() {
   var oldonload = window.onload;
   if (typeof window.onload != 'function') {
      window.onload = captureLoadTime;
   } else {
      window.onload = function() {
         if (oldonload) {
            oldonload();
         }
         captureLoadTime();
      }
   }
}

In Listing 6, you begin by storing the existing onload event in a local variable called oldonload. The rest of the code is simple. If there is already a JavaScript function attached to the existing Web page, you call it (using oldonload), and then you call captureLoadTime. If there is no existing script for the onload event, you just capture the load time of the page by calling captureLoadTime.

Listing 7 shows captureLoadTime and its associated functions.

Listing 7. captureLoadTime and its associated functions

function captureLoadTime() {
   restorePreviousPostTime();
   var docLocation = document.title;
   createCookie('pageLoadName', docLocation );
   createCookie('pageloadTime', getDateString(currentDate) );
   addOnBeforeUnloadEvent();
}
function restorePreviousPostTime() {
   var prevPagePostTime = readCookie('pagepostTime');
   createCookie('prevPagePostTime', prevPagePostTime );
}
function readCookie(name) {
   var ca = document.cookie.split(';');
   var nameEQ = name + "=";
   for(var i=0; i < ca.length; i++) {
     var c = ca[i];
     while (c.charAt(0)==' ') c = c.substring(1, c.length);
     if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
   }
   return null;
}
1 2 3 4 Page 1
Page 1 of 4