Sie sind auf Seite 1von 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
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
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
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;
}

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,
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 imports
public 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 {
this.accessLogFile = new FileWriter(filterConfig.getInitParameter("server.time.log.pa
} catch (IOException e) {
throw new ServletException(e);
}
String sBrowserTimeCaptureEnabled = filterConfig.getInitParameter("browser.time.log.enab
if ( sBrowserTimeCaptureEnabled != null && "true".equalsIgnoreCase(sBrowserTimeCaptureEn
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 t

//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("\" ");
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(" [");
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>
<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

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.
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";
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
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


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

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.
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
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
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
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

Das könnte Ihnen auch gefallen