Measuring Web application response time: Meet the client

20
Sponsored by: This story appeared on JavaWorld at http://www.javaworld.com/javaworld/jw-11-2008/jw-11-measuring-client-response.html Measuring Web application response time: Meet the client Server-side execution is only half of the story By Srijeeb Roy, JavaWorld.com, 11/18/08 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

description

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

Transcript of Measuring Web application response time: Meet the client

Page 1: Measuring Web application response time: Meet the client

Sponsored by:

This story appeared on JavaWorld athttp://www.javaworld.com/javaworld/jw-11-2008/jw-11-measuring-client-response.html

Measuring Web application response time:

Meet the client

Server-side execution is only half of the story

By Srijeeb Roy, JavaWorld.com, 11/18/08

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

Page 2: Measuring Web application response time: Meet the client

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.

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

Page 3: Measuring Web application response time: Meet the client

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.

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

Page 4: Measuring Web application response time: Meet the client

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=/";}

Page 5: Measuring Web application response time: Meet the client

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.

Page 6: Measuring Web application response time: Meet the client

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;}

The captureLoadTime function begins by calling the restorePreviousPostTime function. This is

because you need to keep the pagepostTime cookie saved in some other cookie; otherwise, when the

next server-side call takes place, pagepostTime will be overwritten with the current time. Now,

Page 7: Measuring Web application response time: Meet the client

coming back to the captureLoadTime function, you store the title of the page in a cookie named

pageLoadName. You need to send the page name to the server, not just the times -- otherwise you

won't be able to map application response time to specific pages! For simplicity's sake, in the sample

code I have chosen to use document.title. However, in complex scenarios, you might also have to

send the action event URLs for the action to be fired.

To handle more complex scenarios, simply using onbeforeunload to capture t0 may not be

sufficient. You may need to capture individual events, like clicking the submit button, or clicking

links to get a form action or the link's href values. Later in this article, you'll see how to intercept

these kinds of events. If you can intercept them, then you can get the action value of the form when

the server-side call is initiated. Once you've intercepted a form submit or link click, you can set the

value of the pageName cookie; then, in restorePreviousPostTime, you can save the earlier action

URL in another cookie, called prevPageName, just as you restored the pagepostTime cookie value

using the prevPagePostTime cookie.

You should also note that, inside captureLoadTime, you are at last calling addOnBeforeUnloadEvent

to attach the onbeforeunload event to the window. You already saw how addOnBeforeUnloadEvent

worked in Listing 7.

Now you've got the three cookies that you need: pageLoadName, prevPagePostTime and

pageloadTime. prevPagePostTime holds the value of t0, pageloadTime holds the value of t4, and

pageLoadName holds the title of the document that has executed in an amount of time that can be

expressed as t4 minus t0.

Sending and logging client-side response times

When sending the t0 and t4 values to the server, keep in mind that instrumenting the code should not

add much load to the network. Therefore, you will not send t0 and t4 separately. As mentioned

earlier, when the next server-side request is executed, the cookies will automatically travel to the

server. (Alternately, you could fire an Ajax request to send the values to server, after the page's

onload event; however, doing so will add an extra server-side call, though the overhead for this call

is pretty minor. Such an Ajax-based approach is beyond the scope of this article.)

On the server side, you'll need a Filter to read the cookies (pageLoadName, prevPagePostTime and

pageloadTime) from the HttpServletRequest object. Then you'll calculate the execution time from

pageloadTime and prevPagePostTime. Next, you'll write a logfile with all the details, using

pageLoadName and the execution time.

The filter code will look like Listing 8. The code not only captures the client-side execution time (t4

minus t0), but the server-side execution time (t2 minus t1) as well.

Listing 8. Server-side filter code to log t0, t1, t2, and t4

package com.tcs.tool;//All Required importspublic class HTTPAccessFilter implements Filter { private FileWriter accessLogFile = null; private boolean browserTimeCaptureEnabled = false; private FileWriter clientSideExecutionTimeFile = null;

public void init(FilterConfig filterConfig) throws ServletException { System.out.println("HTTPAccessFilter is loaded..."); try {

Page 8: Measuring Web application response time: Meet the client

this.accessLogFile = new FileWriter(filterConfig.getInitParameter("server.time.log.path")); } catch (IOException e) { throw new ServletException(e); } String sBrowserTimeCaptureEnabled = filterConfig.getInitParameter("browser.time.log.enabled"); if ( sBrowserTimeCaptureEnabled != null && "true".equalsIgnoreCase(sBrowserTimeCaptureEnabled) ) { this.browserTimeCaptureEnabled = true; try {

this.clientSideExecutionTimeFile = new FileWriter( filterConfig.getInitParameter("browser.time.log.path")); } catch (IOException e) { e.printStackTrace(); this.browserTimeCaptureEnabled = false; //throw new ServletException(e); } } }

public void destroy() {}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

long startTime = System.currentTimeMillis();//t1 chain.doFilter(request, response); long endTime = System.currentTimeMillis();//t2

long deltaTime = (endTime-startTime);//in milliseconds HttpServletRequest hReq = (HttpServletRequest) request; String jSessionId = hReq.getSession(true).getId(); String userId = hReq.getRemoteHost(); //for simplicity let us make the userId equals to remoteHostAddress

//Now dealing with (t2-t1) if ( this.accessLogFile != null ) { writeserverSideExecutionTime(hReq, userId, jSessionId, deltaTime); }

//Now dealing with (t4-t0) or (t3-t0) if ( this.browserTimeCaptureEnabled && this.clientSideExecutionTimeFile != null ) { writeBrowserSideExecutionTime(hReq, userId, jSessionId); }

}

private void writeserverSideExecutionTime(HttpServletRequest hReq, String userId, String jSessionId, long deltaTime) {

String remoteAddress = hReq.getRemoteAddr(); SimpleDateFormat dateFormat = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss]"); String endDate = dateFormat.format(new Date()); StringBuffer sb = new StringBuffer(); sb.append(remoteAddress); sb.append(" - - "); sb.append(endDate); sb.append(' '); sb.append(deltaTime); sb.append(" \""); sb.append(hReq.getMethod()); sb.append(" ["); sb.append(hReq.getRequestURI()); sb.append("] ").append(hReq.getProtocol()).append("\" ");

Page 9: Measuring Web application response time: Meet the client

sb.append('"'); sb.append(hReq.getHeader("user-agent")); sb.append('"');

sb.append(" USERID="+userId); if (jSessionId != null) sb.append(" JSESSIONID="+jSessionId); sb.append("\r\n");

try { //writing (t2-t1) accessLogFile.write(sb.toString()); accessLogFile.flush(); }catch (Exception e) { //todo: handle properly e.printStackTrace(); } }

private void writeBrowserSideExecutionTime(HttpServletRequest hReq, String userId, String jSessionId) {

String prevPagePostTime = null; String pageLoadName = null; String pageloadTime = null;

Cookie cookie1[]= hReq.getCookies(); if (cookie1 != null) { for (int i=0; i<cookie1.length; i++) { Cookie cookie = cookie1[i]; if (cookie != null && cookie.getName().equals("pageLoadName")) pageLoadName = cookie.getValue();

if (cookie != null && cookie.getName().equals("prevPagePostTime")) prevPagePostTime = cookie.getValue();

if (cookie != null && cookie.getName().equals("pageloadTime")) pageloadTime = cookie.getValue(); } } if ( pageLoadName == null || pageLoadName.equalsIgnoreCase("null")) pageLoadName = "UNKNOWN"; if ( prevPagePostTime != null && prevPagePostTime.trim().length() > 0 && !prevPagePostTime.trim().equals("null")) {

System.out.println("pageloadTime=" + pageloadTime); System.out.println("pageloadTime=" + prevPagePostTime); Date dtPageloadTime = getClientSideDate(pageloadTime); if ( dtPageloadTime == null ) return; Date dtPrevPagePostTime = getClientSideDate(prevPagePostTime); if ( dtPrevPagePostTime == null ) return; //t4 - t0 long executionTime = (dtPageloadTime.getTime() - dtPrevPagePostTime.getTime() );

SimpleDateFormat dateFormat = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss]"); StringBuffer sb = new StringBuffer(); sb.append(hReq.getRemoteAddr()); sb.append(" - - "); sb.append(dateFormat.format(dtPageloadTime)); sb.append(" "); sb.append(executionTime); sb.append(" \""); sb.append(hReq.getMethod()); sb.append(" [");

Page 10: Measuring Web application response time: Meet the client

sb.append(pageLoadName); sb.append("] ").append(hReq.getProtocol()).append("\" "); sb.append('"'); sb.append(hReq.getHeader("user-agent")); sb.append('"'); if (userId != null) sb.append(" USERID="+userId); if (jSessionId != null) sb.append(" JSESSIONID="+jSessionId); sb.append("\r\n");

try { //writing (t4-t0) clientSideExecutionTimeFile.write(sb.toString()); clientSideExecutionTimeFile.flush(); }catch (Exception e) { // todo: handle properly e.printStackTrace(); }

} }

private Date getClientSideDate(String inputTime) { if ( inputTime == null ) return null; inputTime = inputTime.trim(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/M/d HH:mm:ss:SSS"); try { return dateFormat.parse(inputTime); } catch (ParseException e) { e.printStackTrace(); } return null; }

}

In Listing 8, in the Filter's init() method you are setting a few variables, like accessLogFile,

browserTimeCaptureEnabled, and clientSideExecutionTimeFile, from the Filter's init

parameters, which should be defined in web.xml.

In the doFilter() method, the steps are straightforward. First, you call

writeserverSideExecutionTime() to log the server-side execution time (t2 minus t1). Then you

call writeBrowserSideExecutionTime() to log the value of t4 minus t0.

In writeBrowserSideExecutionTime(), you are extracting the values of the three cookies

(pageLoadName, prevPagePostTime, and pageloadTime) and then converting the String values of

prevPagePostTime and pageloadTime into java.util.Date objects and calculating the time

difference between these two date values. Finally, you write the time difference in the client-side

execution time file, along with a few other details.

The web.xml entry should contain the code shown in Listing 9.

Listing 9. web.xml entry for HTTPAccessFilter

<filter> <filter-name>HTTPAccessFilter</filter-name> <filter-class>com.tcs.tool.HTTPAccessFilter</filter-class> <init-param> <param-name>server.time.log.path</param-name> <param-value>C:/logs/HTTPserverAccess.txt</param-value> </init-param>

Page 11: Measuring Web application response time: Meet the client

<init-param> <param-name>browser.time.log.enabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>browser.time.log.path</param-name> <param-value>C:/logs/HTTPClientSideAccess.txt</param-value> </init-param></filter>

The init parameters mention some file paths. The code in Listing 9 assumes that you are running the

application under the Windows operating system, and that you have already created a directory

named logs on your C drive. (If you're using a non-Windows OS, please modify this XML as

needed.) Remember that you also have to mention the filter-mapping -- that is, the URL patterns

or servlet requests that should be intercepted by this Filter -- in web.xml.

In the example as you've seen it so far, the FileWriter APIs are used to write the logs. You could

also use Apache Commons logging, or indeed any of your favorite logging frameworks. Doing so

would require changes both to the Filter code and to web.xml.

Inserting the instrumentation scripts into your Web page

Now another question remains: how can you add this JavaScript to your Web page? You could of

course put most of the needed JavaScript code in a .js file and include that file in all the application

pages. But there are two other options to explore:

Most projects have a common JavaScript file. You could put all of the common JavaScript

methods inside that file. You then have to consider the JavaScript calls that need to be executed

at the end of the page. Generally, most projects have a separate footer.jsp (or similar) page that

is included in all other JSP pages. Put those JavaScript calls inside that footer file.

Write another filter that can modify your response. The filter will examine responses if they are

HTML-based; if that's the case, the filter will add a script file reference in the head of the

HTML response. The filter will also insert a few JavaScript calls that need to be executed at

the end of the page.

We'll take the second approach for the following example. But keep in mind that the sample filter

offered here is written only as a proof of concept. It would need modifications before being used in a

production environment.

In the application source code for this article I have consolidated all JavaScript code inside a

JavaScript file name client_time_capture.js. This JavaScript should reside in a directory named

timecapturejs under the root of your Web container.

You need to modify the HTTP content of the HTML-based response and insert the line in Listing 10

before the </head> tag.

Listing 10. JavaScript snippet to be inserted in the head section of the Web page

<script language='JavaScript' src='/YourContextRoot/timecapturejs/client_time_capture.js' type='text/javascript'></script>

You also need to insert the content in Listing 11 before the </body> tag. That way, when the page is

loaded, the addLoadEvent JavaScript function is called.

Page 12: Measuring Web application response time: Meet the client

Listing 11. JavaScript snippet to be inserted just before the </body> tag of the Web page

<script language='JavaScript'>addLoadEvent();</script>

Now take a look at Listing 12, which is a snippet of the new Filter.

Listing 12. Portion of the new Filter

public class ScriptInjectionFilter implements Filter {

//all required other methods//...//...

public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {

//.. HttpServletRequest httpRequest = (HttpServletRequest) request; CharResponseWrapper wrappedResponse = new CharResponseWrapper((HttpServletResponse) response); try { chain.doFilter(request, wrappedResponse); byte[] bArray = wrappedResponse.getData(); if ( bArray != null && bArray.length > 0 && getShouldInject(wrappedResponse) ) { bArray = modifyContent(bArray,httpRequest); } if ( bArray != null ) { response.getOutputStream().write(bArray); response.getOutputStream().close(); } } catch (IOException ioe) { throw ioe; } catch (ServletException se) { throw se; } catch (RuntimeException rte) { throw rte; } } private boolean getShouldInject(ServletResponse response) { // does the contentType allow HTML injection? String contentType = response.getContentType(); String strContentType = (contentType != null) ? contentType.toLowerCase() : ""; if ((strContentType.indexOf("text/html") == -1) && (strContentType.indexOf("application/xhtml+xml") == -1)) { // don't inject anything if the content is not html return false; } return true; }

private static final String startScript1 = "<script language='JavaScript' src='"; private static final String endScript1 = "/timecapturejs/client_time_capture.js' type='text/javascript'></script>\n"; private static final String startScript2 = "<script language='JavaScript'>\naddLoadEvent();\n</script>\n";

Page 13: Measuring Web application response time: Meet the client

private byte[] modifyContent(byte[] array, HttpServletRequest httpRequest) { String sContent = new String(array); StringBuffer sbf = new StringBuffer(sContent); sContent = sContent.toLowerCase(); int indexOfEndHead = sContent.indexOf("</head>"); if ( indexOfEndHead != -1 ) { sbf = sbf.insert(indexOfEndHead, startScript1 + httpRequest.getContextPath() + endScript1); int indexOfEndBody = sbf.toString().toLowerCase().indexOf("</body>"); if ( indexOfEndBody != -1 ) { sbf = sbf.insert(indexOfEndBody, startScript2); return sbf.toString().getBytes(); } else { return array; } } else { return array; } }}

In ScriptInjectionFilter's doFilter() method, you are wrapping the HttpServletResponse

object in a class named CharResponseWrapper. CharResponseWrapper's code is supplied in the

article's sample code. CharResponseWrapper extends HttpServletResponseWrapper. The main

purpose of this wrapper is to get the HTTP response content written in a temporary character array,

instead of using the PrintWriter's default writer to write the response to the output stream directly.

You can modify this character array to insert your JavaScript snippets and take the responsibility

yourself to write it to the output stream.

After wrapping the response, you call FilterChain's doFilter() method. This method executes the

actual request; afterwards, you get the value using wrappedResponse.getData(). If the size of the

content is greater than 0 the content is HTML, then you modify the content by calling the

modifyContent() method. Inside modifyContent(), you search for the </head> tag and insert the

reference to the client_time_capture.js file before that tag.

Next, you search for the </body> tag and insert the addLoadEvent JavaScript function call. Finally,

in the doFilter() method, you write the modified content in HTTPServletResponse's output stream.

Listing 13 shows the entry for this Filter that you'll have to add to web.xml.

Listing 13. web.xml entry for ScriptInjectionFilter

<filter> <filter-name>ScriptInjectionFilter</filter-name> <filter-class>com.tcs.tool.filter.ScriptInjectionFilter</filter-class></filter><filter-mapping> <filter-name>ScriptInjectionFilter</filter-name> <url-pattern>*.jsp</url-pattern></filter-mapping>

The filter-mapping in Listing 13 assumes that your pages are JSP pages and are accessed from the

browser with the .jsp extension only. If you were using a Struts-based application, you'd need to add

another filter-mapping block with the url-pattern *.do. Alternately, you can give the Struts

Action servlet in the <servlet-name> parameter under filter-mapping. Similarly, if you're dealing

Page 14: Measuring Web application response time: Meet the client

with a JSF application, you might have to map *.faces or *.jsf, or the Faces servlet itself, in the

filter-mapping.

The example presented so far offers a reasonably complete look at one way to measure application

response time from the client's perspective. But the techniques outlined won't cover every situation.

Let's consider how you might adapt these techniques for trickier contexts, starting with Ajax-based

calls.

Ajax-based calls

If your application relies heavily on Ajax-based calls, you're no doubt interested in capturing the

client-side timings for those calls. The problem with Ajax is that not all browsers consider the

XMLHttpRequest to be a real JavaScript object -- Internet Explorer 6 treats it as an ActiveX object,

for instance. Therefore, it is difficult to attach functions like onopen and onsend to intercept the open

and send methods of XMLHttpRequest.

The solution is to wrap the original XMLHttpRequest object with a real JavaScript object that

supports intercepting XMLHttpRequest's operation. You can use the open source xmlhttprequest

project (see Resources) to do just that. Once you wrap the XMLHttpRequest in a proper JavaScript

object, then you can intercept method calls like open and send to inject your time-capturing code just

before the send method is called. Listing 14 shows how it's done.

Listing 14. Intercepting the Ajax send function

XMLHttpRequest.prototype.originalSend = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send = captureTimeAjaxSend;

function captureTimeAjaxSend(vData) { createCookie('pagepostTime', getDateString() ); this.originalSend(vData);}

To intercept the return of the Ajax call, I have modified XMLHttpRequest.src.js (the JavaScript file

from the xmlhttprequest open source project mentioned above) slightly. If you open the

XMLHttpRequest.src.js file supplied with this article, you'll find that I have added the code in Listing

15 into it. (These are at lines 176 through 180 in the file.)

Listing 15. Modifying XMLHttpRequest.src.js to intercept the Ajax return call

if (oRequest.readyState == cXMLHttpRequest.DONE) { try { captureTimeAjaxReturn(oRequest); }catch(e){};}

The code in Listing 15 checks to see if the readyState of the wrapped XMLHttpRequest object is

DONE or not (state 4). If the request is completed, then it calls captureTimeAjaxReturn, and the

XMLHttpRequest is passed. In Listing 16, you can note down the load time. The

captureTimeAjaxReturn method will be called after the developer's function (which has been

registered using onreadystatehandler).

Listing 16. Capturing t4 for the Ajax return call

Page 15: Measuring Web application response time: Meet the client

function captureTimeAjaxReturn(xmlHttp) { //capture the load time}

Remember that you must explicitly include the reference to XMLHttpRequest.src.js in your pages.

The script insertion Filter supplied with the sample code does not inject this script.

Capuring submit events and link clicks

Earlier, you saw how to capture the initial time of a request from the browser (t0) for most

server-side initiations, by overriding the onbeforeunload function. In some cases you might want to

capture different points, like submit events or link clicks. Let's consider these two scenarios in turn.

Submit events

Before submitting the page, you have to intercept it and write code inside it so as to write the t0 value

into a cookie. There are two ways a submit event can happen: initiated by user (not a JavaScript

submit) from the input type's submit attribute, or as a JavaScript submit method on a form object.

The two types of submit events need to be intercepted in different ways.

A user-initiated auto submit might happen when the user clicks on a form with one of the attributes

shown in Listing 17.

Listing 17. User-initiated auto submit examples

<input type="submit" .../><input type="image" .../>

To capture user-initiated auto submit, you must attach an onsubmit JavaScript event to every form on

your Web page, as shown in Listing 18.

Listing 18. Capturing submit events for user-initiated auto submit

function captureSubmit() { try { var allForms = document.forms; if ( allForms != null ) { for ( var i=0; i < allForms.length; i++) { allForms[i].onsubmit = capturePostTimeForSubmit; } } }catch(e){} return true;}

Listing 18 loops through all the forms in the document and registers a custom function

(capturePostTimeForSubmit) with an onsubmit event to each of them. Inside the

capturePostTimeForSubmit method, you get the current time and write it inside the pagepostTime

cookie, as shown in Listing 19.

Listing 19. Capturing t0 for user-initiated auto submit

function capturePostTimeForSubmit(event) { createCookie('pagepostTime', getDateString());

Page 16: Measuring Web application response time: Meet the client

}

As noted, you may also need to capture JavaScript submits. There's a problem, though: if you register

the onsubmit handler with a form that uses a JavaScript submit event, the handler will not be called

when the submit function executes. Therefore, in such a case you'd need to overwrite the submit

function itself instead of attaching the onsubmit handler. Listing 20 shows how you'd do it.

Listing 20. Capturing the JavaScript submit function call

function captureJavaScriptSubmit() { for (var form, i = 0; (form = document.forms[i]); ++i) { form.realSubmit = form.submit form.submit = function () { if ( capturePostTimeForJavaScriptSubmit(this) ) this.realSubmit(); } }}

In Listing 20, you first loop through all the forms in the document, first storing the actual submit

method in form.realSubmit. Then you override the original submit method and attach anonymous

JavaScript to it. Inside the anonymous function, you call the

capturePostTimeForJavaScriptSubmit method; inside that method, you capture the current time

and write it inside the cookie, as shown in Listing 21.

Listing 21. Capturing t0 for the JavaScript submit function call

function capturePostTimeForJavaScriptSubmit(aForm) { createCookie('pagepostTime', getDateString() );}

You may wonder why you're using capturePostTimeForJavaScriptSubmit instead of

capturePostTimeForSubmit. You could use the latter method, but keep in mind that you've passed

two different types of parameters in these two methods. From these parameters, you can access

properties like action that are being fired inside the capturePostTimeForSubmit and

capturePostTimeForJavaScriptSubmit methods, and you can send this information to the server.

As the object types are different, so the procedure for extracting the action attribute of the form is

also different. Hence, to separate the process, you need both methods.

Capturing submit events in Apache MyFaces/JSF

I've noted an interesting behavior when using these techniques to capture submit events in an

application built with the Apache MyFaces implementation of JavaServer Faces. In the application, if

I used an h:commandLink tag in a JSF page, then MyFaces would generate JavaScript to initiate the

server call. Though the script generated by MyFaces used a JavaScript call to submit the page, before

performing that action the script checks to see if an onsubmit handler is attached to the form being

submitted. If an onsubmit event is attached, the generated JavaScript calls the onsubmit function

associated with it. As a result, both capturePostTimeForSubmit and

capturePostTimeForJavaScriptSubmit were called before submit -- capturePostTimeForSubmit

was called for the onsubmit handler that I attached, and capturePostTimeForJavaScriptSubmit

was called for the JavaScript submit. However, as both the methods are doing the same job, the

functionality is not broken. The result is that the pagepostTime cookie is written twice before the

submit.

Page 17: Measuring Web application response time: Meet the client

For example, in capturePostTimeForSubmit, you can extract the action attribute of the form as in

Listing 22.

Listing 22. Extracting the action attribute for user-initiated auto submit

function capturePostTimeForSubmit(event) { var target = event ? event.target : this; var actionValue = "UNKNOWN"; if ( target.action != null && target.action != "undefined") { actionValue = target.action; //Now you can write the action in a cookie and send it to server to track } //old code createCookie('pagepostTime', getDateString());}

The equivalent for a JavaScript submit is illustrated in Listing 23.

Listing 23. Extracting the action attribute for JavaScript submit

function capturePostTimeForJavaScriptSubmit(aForm) { var actionValue = "UNKNOWN"; if ( aForm.action != null && aForm.action != "undefined") { actionValue = aForm.action; //Now you can write the action in a cookie and send it to server to track } //old code createCookie('pagepostTime', getDateString()); return true;}

However, these two functions could be merged into one by implementing a few more conditional

statements inside. I leave that as an exercise for the reader.

As in the earlier scenario, the captureJavaScriptSubmit function needs to be called at the end of

the page.

Link clicks

Capturing link clicks is little bit tricky. Consider the following examples:

<a href="http://myserver/CTXRoot/Test.jsp">just a link (no onclick)</a>: Just a

plain link. Clicking on it takes the user to another Web page.

<a href="http://myserver/CTXRoot/Test.jsp" onclick="return

fromLink('Hello')">just a link (with onclick)</a>: An additional JavaScript function,

fromLink, is attached to the link. The fromLink function is a developer-defined function that

executes some logic and returns a boolean value. If the return value is true, the URL

mentioned in the href attribute, http://myserver/CTXRoot/Test.jsp, is opened. If the return

value is false, then nothing happens. Hence, you only need to capture t0 if the return value of

the function is true.

<a href="javascript:alert(1)">just a link (href contains javascript)</a>: The

href attribute contains only JavaScript. Here, you should not capture t0 under any

circumstance.

<a href="#" onclick="someMethod()">just a link (href contains #, onclick

contains a JavaScript method)</a>: Here the href attribute contains a value of "#". You

Page 18: Measuring Web application response time: Meet the client

should also not capture t0 in this case. Recall from the discussion of the JavaScript method

mentioned in regards to onclick that you can submit a form using the form.submit method;

in that case, t0 will automatically be captured by the JavaScript submit handling.

Keeping these scenarios in mind, look at the JavaScript functions in Listing 24.

Listing 24. Capturing t0 for various link click scenarios

function captureLinkClicks() {01: try {02: var links = document.getElementsByTagName('a');03: for (var i = 0; i < links.length; i++) {04: var hrefString = links[i].href + "";05: if ( hrefString.indexOf("#") == -1 && hrefString !== ""06: && !hrefContainsScript(hrefString) ) {07: if ( links[i].onclick ) {08: links[i].oldonclick = links[i].onclick;09: }10: links[i].onclick = function() {11: var ret = false;12: if (this.oldonclick) {13: ret = this.oldonclick();14: if ( false != ret ) {15: capturePostTimeForLink(this);16: }17: return ret;18: }19: else {20: capturePostTimeForLink(this);21: return true;22: }23: }24: }25: }26: }catch(e){}27: return true; }function hrefContainsScript(hrefString) { hrefString = hrefString.toUpperCase(); hrefString = trim(hrefString); if ( hrefString.substring(0,10) == "JAVASCRIPT") { return true; } return false;}function trim(str) { var a = str.replace(/^\s+/, ''); return a.replace(/\s+$/, '');}function capturePostTimeForLink(aLink) { createCookie('pagepostTime', getDateString() );}

The captureLinkClicks function loops through all the links present in the document and then

checks to see if you really need to pump the code to capture t0. If the href attribute of the anchor tag

contains "#", then you should not capture t0. The same is true if the href attribute is set to a blank

string, or if the href attribute contains JavaScript instead of a URL. Lines 04 and 05 determine

whether any of those conditions are true.

Now imagine that the href attribute of the anchor tag contains a URL. In that case, you'd need to

check to see if any existing JavaScript onclick functions are already attached to the anchor tag. If an

Page 19: Measuring Web application response time: Meet the client

onclick event already exists, then you store the existing onclick event to another attribute, named

oldonclick, in that anchor object itself. Next, you override the onclick function of the anchor

object. If any oldonclick event is attached to the anchor object, you call that event.

Be aware that the old onclick may look something like Listing 25.

Listing 25. onclick of a link containing multiple function calls

onclick="alert(1);alert(2);somefunc('some param')"

In other words, onclick may not contain a single JavaScript function call. In this approach, all the

JavaScript that is written inside the existing onclick will be executed when Line 12 from Listing 24

executes. Again, remember that Line 12 will not execute when captureLinkClicks is called. Here,

you're actually declaring an anonymous function on the link's onclick event. When the user clicks

the link, only Line 12 is executed.

Now, if the onclick method returns false, then the browser does not fire the URL. Therefore, Line

13 checks to see if the return value of the oldonclick event is false or not. If the return value is not

false, then you should capture t0. If there is no onclick already associated in the anchor tag, then

you should capture t0 and return true from the anonymous JavaScript function (Lines 20 and 21 of

Listing 24).

The other helper functions, such as hrefContainsScript and trim, should be self-explanatory.

Conclusions and caveats

In this article I have introduced an approach to capturing the end-user response time for various

scenarios, using code samples to demonstrate (and prove) the concept. In dealing with this article's

sample code, however, you should keep in mind following points:

Scripts in this article have been tested in Internet Explorer 6.0, Mozilla Firefox 2.0.0.11, and

Safari 3.1.2 for Windows XP. As the approach mentioned is heavily dependent on JavaScript,

and JavaScript's behavior varies among different browsers and browser versions, you may

have to change or tweak the code to work on other browsers, or even with other versions of the

tested browsers.

As the approach relies on cookies and JavaScript, it will obviously fail if JavaScript or cookies

are disabled on the end user's PC. In addition, the cookie size limitation applies. Some

browsers, like Firefox and Safari, run only one executable, even if the user launches more than

one instance of the browser. For all those instances, the browser shares the same memory space

for cookies. Hence, parallel access to your application from different browser windows on the

same machine will produce inconsistent instrumentation results.

As the approach injects JavaScript dynamically, make sure to do proper testing to ensure that

the JavaScript code that is injected by your instrumentation does not adversely affect your

original application code and change the application's behavior.

You may have already guessed that the time for the first and last page loads cannot be captured

using this approach.

You can enhance the ScriptInjectionFilter and HTTPAccessFilter code to enable or

disable script insertion and logging at runtime. This way, script injection and logging can be

controlled without changing web.xml or even rebooting the server. Keep a variable in

application scope (ServletContext), and control the value of that variable from some

administration screen. In the filters, change the code accordingly to enable or disable logging

Page 20: Measuring Web application response time: Meet the client

and script injection.

The server-side logfile uses the server's timestamp while writing the logs; the client-side logfile

uses client's timestamp. The response time will be calculated correctly, as t1 and t2 are both

based on the server's timestamp, and t0 and t4 based on the client's timestamp. However, if an

end user changes the system date on the client machine between a server call and the loading

of the next page, then t0 and t4 will not be consistent.

The approach outlined here may not work properly if multiple frames or framesets are

involved in a page.

If the developer has already written an onbeforeunload function to provide end users with a

warning if they're about to navigate away from an existing page, then t0 is recorded before that

warning dialog appears. Hence, effectively the time you get by subtracting t0 from t4 will also

include the time the user spends thinking until dismissing that warning.

Finally, I have tested the approach with three different applications. One is based on JSF,

another on Struts, and the third on the MVC 1 architecture (all JSPs, no Controller servlet). I

have used IBM WebSphere 6.1 as the application server. I've also tested a few other

applications in Tomcat 5.5 after applying the instrumentation code within the applications.

Developers typically only record server-side execution time, ignoring actual end-user response time.

I hope that you emerge from this article with new ideas about recording your application's actual

execution time, including the end user's perspective. You've also learned a minimal-effort way to

insert instrumentation JavaScript automatically into all of your application Web pages. You can use a

similar approach to modify your HTTP responses for any other purpose.

About the author

Srijeeb Roy holds a bachelor's degree in computer science and engineering from Jadavpur University

in Kolkata, India. He is currently working as an enterprise architect at Tata Consultancy Services

Limited. He has been working in Java SE and Java EE for more than nine years, and has a total

experience of more than 10 years in the IT industry. He has developed several in-house frameworks

and reusable components in Java for his company and clients. He has also worked in areas such as

Forte, CORBA, and Java ME.

All contents copyright 1995-2009 Java World, Inc. http://www.javaworld.com