· 4 years ago · Jul 14, 2021, 04:22 AM
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2<html><head>
3
4
5<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"><title></title>
6
7
8 <style type="css">
9 body, td, pre {
10 font-size: x-small;
11 font-family: Verdana, Arial, Helvetica, sans-serif;
12 color: #000;
13 font-size: small;
14 font-family: Verdana, Arial, Helvetica, sans-serif;
15 color: #000;
16 }
17 code, pre {
18 margin: 0;
19 padding: 0;
20 }
21 </style>
22
23<!--ao: abstract and blurb commented:
24
25ABSTRACT: If an application relies heavily on JavaScript or some other client-side scripting then it isn't just server-side execution time that should be considered while measuring overall application performance; client-side script execution should also be taken into account. In fact, if you're measuring the execution time from the end-user perspective, the network time needs to be considered. This article explores a lightweight approach to capturing what the end user would perceive as an application's actual response time: server-side execution time, plus network time, plus client-side script execution time that must be completed before a page is loaded. The article also explores one approach for sending and logging all of the client-side response times on a server for future analysis.
26
27TOC_BLURB: Trying to measure how fast your application is? If you're not seeing things from the user's perspective, you're not getting the full picture.
28
29-->
30
31</head>
32<body>
33
34<h1>Measuring Web application response time from the end user's perspective</h1>
35
36<h2>You've timed sever-side processing -- but are you forgetting the client?</h2>
37
38<p>In a Java EE application, it is very easy to capture the overall server-side execution time of a Web request. Often, a developer will write a <code>Filter</code> (implementing <code>javax.servlet.Filter</code>) 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 <code>Filter</code>, the developer stores the current time; when the <code>Filter</code> handle returns after executing the <code>doFilter()</code> method, the developer stores 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.</p>
39
40<p>There are several tools available 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 deploy the EAR or WAR to instrument your application. The open source <a href="#resources">Jensor</a> project provides excellent bytecode instrumentation for server-side code. These kind of tools are very useful, but sometimes it becomes necessary to capture the actual end-user response times; these include not only the time it takes code to execute on the server, but also the time it takes to send information over the network, and the time it takes client-side JavaScript to execute. I have seen applications that process server-side requests very quickly, but run slowly because of network bottlenecks. And sometimes developers write JavaScript in the page's <code>onload</code> method that's so convoluted that it takes a considerable amount of time to execute, even after the response has travelled back to the browser.</p>
41
42<h2>Capturing the client-side experience</h2>
43
44<p>It is very difficult to write a generic tool that can provide client-side timings like those captured on the server side and doesn't result in a major performance hit in the actual application. But you can take few steps while developing your application to measure client-side performance. The approach I'll outline in this article will allow you to capture the actual response time that the end user will experience in most cases. Keep in mind that this is not a generic tool that can be applied blindly in all cases; but, in many projects, these concepts can come in handy.</p>
45
46<p>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.</p>
47
48<a name="fig1">
49<img src="fig1.gif" width="869" height="606" alt="Time captures at different points" />
50<h4>Figure 1. Time captures at different points</h4></a>
51
52<p>In the scenario 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 <code>Filter</code> at t1. Next, the request is forwarded to a servlet and to JSPs (or perhaps to a POJO or EJBs). Then the call returns to the <code>Filter</code> 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 end at this point. The response traverses back to browser. Say it reaches the browser at t3. If the Web page has a onload method in it, that needs to be executed; 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.</p>
53
54<p>This article will begin by considering 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:</p>
55
56<ul>
57<li>Capturing t0 and t4</li>
58<li>Sending the t0 and t4 values to server</li>
59</ul>
60
61<p>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 purposes.</p>
62
63<a name="fig2">
64 <img src="fig2.gif" width="659" height="819" alt="Capturing end-user time values" />
65<h4>Figure 2. Capturing end-user time values</h4></a>
66
67<table align="right" border="1" width="40%">
68<tbody><tr>
69<th bgcolor="#0000c0">
70<font color="#ffffff">Sample code</font></th>
71</tr>
72<tr>
73<td><p><a href="#resources">clientsidetimecapture-src.zip</a> 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.</p>
74
75<p>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.</p></td>
76
77</tr>
78</tbody>
79</table>
80
81<h2>Capturing t0</h2>
82
83<p>To capture 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 <code>form.submit</code>, <code>window.open</code>, or <code>window.showModalDialog</code> methods, or even calls <code>location.replace</code>. However the request is initiated, you must intercept it and capture the current time before initiation.</p>
84
85<p>Most of the server-side initiations I just mentioned -- those that replace the current page -- can be intercepted by the <code>window.onbeforeunload</code> event. Therefore, if you can attach an <code>onbeforeunload</code> event to the window object, you can capture t0 when the <code>onbeforeunload</code> event is fired. Take a look at the JavaScript functions in Listing 1 to see how this could work.</p>
86
87<h4>Listing 1. Using the onbeforeunload event to capture t0</h4>
88
89<pre>
90<code>
91function addOnBeforeUnloadEvent() {
92 var oldOnBeforeUnload = window.onbeforeunload;
93 window.onbeforeunload = function() {
94 var ret;
95 if ( oldOnBeforeUnload ) {
96 ret = oldOnBeforeUnload();
97 }
98 captureTimeOnBeforeUnload();
99 if ( ret ) return ret;
100 }
101}
102
103function captureTimeOnBeforeUnload() {
104 //capturing t0 here
105 createCookie('pagepostTime', getDateString());
106}
107</code>
108</pre>
109
110<p>In the <code>addOnBeforeUnloadEvent</code> function, you are first storing the window's existing <code>onbeforeunload</code> event. Then you override the <code>onbeforeunload</code> 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 <code>onbeforeunload</code> event already attached to the window. If there is, you call that function. Then you call <code>captureTimeOnBeforeUnload</code> to capture t0.</p>
111
112<p>In the <code>captureTimeOnBeforeUnload</code> function, you have used two more JavaScript methods: <code>getDateString</code> and <code>createCookie</code>. These are shown in more detail in Listings 2 and 3.</p>
113
114<h4>Listing 2. getDateString</h4>
115
116<pre>
117<code>
118function getDateString() {
119 var dt1 = new Date();
120 var dtStr = dt1.getFullYear() + "/" + (dt1.getMonth() + 1)+
121 "/" + dt1.getDate() + " " + dt1.getHours() + ":" +
122 dt1.getMinutes() + ":" + dt1.getSeconds()+ ":" +
123 dt1.getMilliseconds();
124 return dtStr;
125}
126</code>
127</pre>
128
129<p>The <code>getDateString</code> method creates an instance of the JavaScript <code>Date</code> object. You create the date string by calling methods like <code>getFullYear</code>, <code>getMonth</code>, and the like on the <code>Date</code> object.</p>
130
131<h4>Listing 3. createCookie</h4>
132
133<pre>
134<code>
135function createCookie(name, value, days) {
136 var expires = "";
137 if (days) {
138 var date = new Date();
139 date.setTime(date.getTime()+(days*24*60*60*1000));
140 expires = "; expires=" + date.toString();
141 }
142 else expires = "";
143 document.cookie = name+"="+value+expires+"; path=/";
144}
145</code>
146</pre>
147
148<p>Listing 3 shows you how to write a cookie using JavaScript. Please note that, while creating the cookie, you are not passing any values for the <code>days</code> 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 <code>days</code> parameter has not been used.</p>
149
150<p>Most server-side initiation points can be intercepted, and you can write a <code>pagepostTime</code> cookie that will contain the t0 value. However, a few server-side initiation points cannot be captured using <code>onbeforeunload</code>. For instance, the <code>window.open</code> method opens a new window without replacing the current page; as the parent page is not unloaded, the <code>onbeforeunload</code> event will not be generated. Another example is the <code>window.showModalDialog</code> function (which is not supported in all browsers).</p>
151
152<p>Hence, for server-side initiation that does not unload the existing page, you need to consider a different approach: intercepting the <code>window.open</code> or <code>window.showModalDialog</code> functions. Look at the JavaScript code snippet in Listing 4 to see how you can override the <code>window.open</code> method.</p>
153
154<h4>Listing 4. Capturing t0 for window.open</h4>
155
156<pre>
157<code>
158var origWindowOpen = window.open;
159window.open = captureTimeWindowOpen;
160
161function captureTimeWindowOpen() {
162 createCookie('pagepostTime', getDateString());
163 if (args.length == 1) return origWindowOpen(args[0]);
164 else if (args.length == 2) return origWindowOpen(args[0],args[1]);
165 else if (args.length == 3) return origWindowOpen(args[0],args[1],args[2]);
166}
167</code>
168</pre>
169
170<p>You can probably guess from Listing 4 that you'll be storing the original <code>window.open</code> method in <code>origWindowOpen</code>. Then you override <code>window.open</code> with the <code>captureTimeWindowOpen</code> function. Inside <code>captureTimeWindowOpen</code>, you again create the <code>pagepostTime</code> cookie with the current time value, and then call the original <code>window.open</code> method (which earlier you stored in <code>origWindowOpen</code>).</p>
171
172<p>Listing 5 illustrates how you can override the <code>window.showModalDialog</code> method.</p>
173
174<h4>Listing 5. Capturing t0 for window.showModalDialog</h4>
175
176<pre>
177<code>
178var origWindowMD = window.showModalDialog;
179window.showModalDialog = captureTimeShowModalDialog;
180
181
182function captureTimeShowModalDialog() {
183 var args = captureTimeShowModalDialog.arguments;
184 createCookie('pagepostTime', getDateString() );
185 if (args.length == 1) return origWindowMD(args[0]);
186 else if (args.length == 2) return origWindowMD(args[0],args[1]);
187 else if (args.length == 3) return origWindowMD(args[0],args[1],args[2]);
188}
189</code>
190</pre>
191
192<p>Listing 5 should require no further explanation; it works in exactly the same fashion as <code>window.showModalDialog</code>.</p>
193
194<h2>Capturing t4</h2>
195
196<p>To capture t4, you will use the <code>onload</code> event. Keep in mind, though, that a page may not have any <code>onload</code> 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, this article will refer to t4 throughout. To make up for the potential lack of an <code>onload</code> method, you'll attach one to the <code>window</code> object using anonymous JavaScript. If the page already has an <code>onload</code> 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 <code>onload</code> function.</p>
197
198<h4>Listing 6. Using the onload function to capture t4</h4>
199
200<pre>
201<code>
202function addLoadEvent() {
203 var oldonload = window.onload;
204 if (typeof window.onload != 'function') {
205 window.onload = captureLoadTime;
206 } else {
207 window.onload = function() {
208 if (oldonload) {
209 oldonload();
210 }
211 captureLoadTime();
212 }
213 }
214}
215</code>
216</pre>
217
218<p>
219In Listing 6, you begin by storing the existing <code>onload</code> event in a local variable called <code>oldonload</code>. The rest of the code is simple. If there is already a JavaScript function attached to the existing Web page, you call it (using <code>oldonload</code>), and then you call <code>captureLoadTime</code>. If there is no existing script for the <code>onload</code> event, you just capture the load time of the page by calling <code>captureLoadTime</code>.
220</p>
221
222<p>Listing 7 shows <code>captureLoadTime</code> and its associated functions.</p>
223
224<h4>Listing 7. captureLoadTime and its associated functions</h4>
225
226<pre>
227<code>
228function captureLoadTime() {
229 restorePreviousPostTime();
230 var docLocation = document.title;
231 createCookie('pageLoadName', docLocation );
232 createCookie('pageloadTime', getDateString(currentDate) );
233 addOnBeforeUnloadEvent();
234}
235function restorePreviousPostTime() {
236 var prevPagePostTime = readCookie('pagepostTime');
237 createCookie('prevPagePostTime', prevPagePostTime );
238}
239function readCookie(name) {
240 var ca = document.cookie.split(';');
241 var nameEQ = name + "=";
242 for(var i=0; i < ca.length; i++) {
243 var c = ca[i];
244 while (c.charAt(0)==' ') c = c.substring(1, c.length);
245 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
246 }
247 return null;
248}
249</code>
250</pre>
251
252<p>The <code>captureLoadTime</code> function begins by calling the <code>restorePreviousPostTime</code> function. This is because you need to keep the <code>pagepostTime</code> cookie saved in some other cookie; otherwise, when the next server-side call takes place, <code>pagepostTime</code> will be overwritten with the current time. Now, coming back to the <code>captureLoadTime</code> function, you store the title of the page in a cookie named <code>pageLoadName</code>. 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 <code>document.title</code>. However, in complex scenarios, you might also have to send the action event URLs for the action to be fired.</p>
253
254<p>To handle more complex scenarios, simply using <code>onbeforeunload</code> 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 <code>href</code> 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 <code>pageName</code> cookie; then, in <code>restorePreviousPostTime</code>, you can save the earlier action URL in another cookie, called <code>prevPageName</code>, just as you restored the <code>pagepostTime</code> cookie value using the <code>prevPagePostTime</code> cookie.</p>
255
256<p>You should also note that, inside <code>captureLoadTime</code>, you are at last calling <code>addOnBeforeUnloadEvent</code> to attach the <code>onbeforeunload</code> event to the window. You already saw how <code>addOnBeforeUnloadEvent</code> worked in Listing 7.</p>
257
258<p>Now you've got the three cookies that you need: <code>pageLoadName</code>, <code>prevPagePostTime</code> and <code>pageloadTime</code>. <code>prevPagePostTime</code> holds the value of t0, <code>pageloadTime</code> holds the value of t4, and <code>pageLoadName</code> holds the title of the document that has executed in an amount of time that can be expressed as t4 minus t0.</p>
259
260<h3>Sending the t0 and t4 values to server</h3>
261
262<p>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 <code>onload</code> 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.)</p>
263
264<p>On the server side, you'll need a <code>Filter</code> to read the cookies (<code>pageLoadName</code>, <code>prevPagePostTime</code> and <code>pageloadTime</code>) from the <code>HttpServletRequest</code> object. Then you'll calculate the execution time from <code>pageloadTime</code> and <code>prevPagePostTime</code>. Next, you'll write a logfile with all the details, using <code>pageLoadName</code> and the execution time.</p>
265
266<p>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.</p>
267
268<h4>Listing 8. Server-side filter code to log t0, t1, t2, and t4</h4>
269
270<pre>
271<code>
272package com.tcs.tool;
273//All Required imports
274public class HTTPAccessFilter implements Filter {
275 private FileWriter accessLogFile = null;
276 private boolean browserTimeCaptureEnabled = false;
277 private FileWriter clientSideExecutionTimeFile = null;
278
279 public void init(FilterConfig filterConfig) throws ServletException {
280 System.out.println("HTTPAccessFilter is loaded...");
281 try {
282 this.accessLogFile = new FileWriter(filterConfig.getInitParameter("server.time.log.path"));
283 } catch (IOException e) {
284 throw new ServletException(e);
285 }
286 String sBrowserTimeCaptureEnabled = filterConfig.getInitParameter("browser.time.log.enabled");
287 if ( sBrowserTimeCaptureEnabled != null && "true".equalsIgnoreCase(sBrowserTimeCaptureEnabled) ) {
288 this.browserTimeCaptureEnabled = true;
289 try {
290
291 this.clientSideExecutionTimeFile =
292 new FileWriter(
293 filterConfig.getInitParameter("browser.time.log.path"));
294 } catch (IOException e) {
295 e.printStackTrace();
296 this.browserTimeCaptureEnabled = false;
297 //throw new ServletException(e);
298 }
299 }
300 }
301
302 public void destroy() {}
303
304 public void doFilter(ServletRequest request, ServletResponse response,
305 FilterChain chain) throws IOException, ServletException {
306
307 long startTime = System.currentTimeMillis();//t1
308 chain.doFilter(request, response);
309 long endTime = System.currentTimeMillis();//t2
310
311 long deltaTime = (endTime-startTime);//in milliseconds
312 HttpServletRequest hReq = (HttpServletRequest) request;
313 String jSessionId = hReq.getSession(true).getId();
314 String userId = hReq.getRemoteHost(); //for simplicity let us make the userId equals to remoteHostAddress
315
316 //Now dealing with (t2-t1)
317 if ( this.accessLogFile != null ) {
318 writeserverSideExecutionTime(hReq, userId, jSessionId, deltaTime);
319 }
320
321 //Now dealing with (t4-t0) or (t3-t0)
322 if ( this.browserTimeCaptureEnabled &&
323 this.clientSideExecutionTimeFile != null ) {
324 writeBrowserSideExecutionTime(hReq, userId, jSessionId);
325 }
326
327 }
328
329 private void writeserverSideExecutionTime(HttpServletRequest hReq,
330 String userId, String jSessionId,
331 long deltaTime) {
332
333 String remoteAddress = hReq.getRemoteAddr();
334 SimpleDateFormat dateFormat =
335 new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss]");
336 String endDate = dateFormat.format(new Date());
337 StringBuffer sb = new StringBuffer();
338 sb.append(remoteAddress);
339 sb.append(" - - ");
340 sb.append(endDate);
341 sb.append(' ');
342 sb.append(deltaTime);
343 sb.append(" \"");
344 sb.append(hReq.getMethod());
345 sb.append(" [");
346 sb.append(hReq.getRequestURI());
347 sb.append("] ").append(hReq.getProtocol()).append("\" ");
348 sb.append('"');
349 sb.append(hReq.getHeader("user-agent"));
350 sb.append('"');
351
352 sb.append(" USERID="+userId);
353 if (jSessionId != null) sb.append(" JSESSIONID="+jSessionId);
354 sb.append("\r\n");
355
356 try {
357 //writing (t2-t1)
358 accessLogFile.write(sb.toString());
359 accessLogFile.flush();
360 }catch (Exception e) {
361 //todo: handle properly
362 e.printStackTrace();
363 }
364 }
365
366 private void writeBrowserSideExecutionTime(HttpServletRequest hReq, String userId,
367 String jSessionId) {
368
369 String prevPagePostTime = null;
370 String pageLoadName = null;
371 String pageloadTime = null;
372
373 Cookie cookie1[]= hReq.getCookies();
374 if (cookie1 != null) {
375 for (int i=0; i<cookie1.length; i++) {
376 Cookie cookie = cookie1[i];
377 if (cookie != null &&
378 cookie.getName().equals("pageLoadName"))
379 pageLoadName = cookie.getValue();
380
381 if (cookie != null &&
382 cookie.getName().equals("prevPagePostTime"))
383 prevPagePostTime = cookie.getValue();
384
385 if (cookie != null &&
386 cookie.getName().equals("pageloadTime"))
387 pageloadTime = cookie.getValue();
388 }
389 }
390 if ( pageLoadName == null ||
391 pageLoadName.equalsIgnoreCase("null")) pageLoadName = "UNKNOWN";
392 if ( prevPagePostTime != null && prevPagePostTime.trim().length() > 0
393 && !prevPagePostTime.trim().equals("null")) {
394
395 System.out.println("pageloadTime=" + pageloadTime);
396 System.out.println("pageloadTime=" + prevPagePostTime);
397 Date dtPageloadTime = getClientSideDate(pageloadTime);
398 if ( dtPageloadTime == null ) return;
399 Date dtPrevPagePostTime = getClientSideDate(prevPagePostTime);
400 if ( dtPrevPagePostTime == null ) return;
401 //t4 - t0
402 long executionTime = (dtPageloadTime.getTime() - dtPrevPagePostTime.getTime() );
403
404 SimpleDateFormat dateFormat = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss]");
405 StringBuffer sb = new StringBuffer();
406 sb.append(hReq.getRemoteAddr());
407 sb.append(" - - ");
408 sb.append(dateFormat.format(dtPageloadTime));
409 sb.append(" ");
410 sb.append(executionTime);
411 sb.append(" \"");
412 sb.append(hReq.getMethod());
413 sb.append(" [");
414 sb.append(pageLoadName);
415 sb.append("] ").append(hReq.getProtocol()).append("\" ");
416 sb.append('"');
417 sb.append(hReq.getHeader("user-agent"));
418 sb.append('"');
419 if (userId != null) sb.append(" USERID="+userId);
420 if (jSessionId != null) sb.append(" JSESSIONID="+jSessionId);
421 sb.append("\r\n");
422
423 try {
424 //writing (t4-t0)
425 clientSideExecutionTimeFile.write(sb.toString());
426 clientSideExecutionTimeFile.flush();
427 }catch (Exception e) {
428 // todo: handle properly
429 e.printStackTrace();
430 }
431
432 }
433 }
434
435 private Date getClientSideDate(String inputTime) {
436 if ( inputTime == null ) return null;
437 inputTime = inputTime.trim();
438 SimpleDateFormat dateFormat =
439 new SimpleDateFormat("yyyy/M/d HH:mm:ss:SSS");
440 try {
441 return dateFormat.parse(inputTime);
442 } catch (ParseException e) {
443 e.printStackTrace();
444 }
445 return null;
446 }
447
448}
449</code>
450</pre>
451
452<p>In Listing 8, in the <code>Filter</code>'s <code>init()</code> method you are setting a few variables, like <code>accessLogFile</code>, <code>browserTimeCaptureEnabled</code>, and <code>clientSideExecutionTimeFile</code>, from the <code>Filter</code>'s <code>init</code> parameters, which should be defined in web.xml.</p>
453
454<p>In the <code>doFilter()</code> method, the steps are straightforward. First, you call <code>writeserverSideExecutionTime()</code> to log the server-side execution time (t2 minus t1). Then you call <code>writeBrowserSideExecutionTime()</code> to log the value of t4 minus t0.</p>
455
456In <code>writeBrowserSideExecutionTime()</code>, you are extracting the values of the three cookies (<code>pageLoadName</code>, <code>prevPagePostTime</code>, and <code>pageloadTime</code>) and then converting the <code>String</code> values of <code>prevPagePostTime</code> and <code>pageloadTime</code> into <code>java.util.Date</code> 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.</p>
457
458<p>The web.xml entry should contain the code shown in Listing 9.</p>
459
460<h4>Listing 9. web.xml entry for HTTPAccessFilter</h4>
461
462<pre>
463<code>
464<filter>
465 <filter-name>HTTPAccessFilter</filter-name>
466 <filter-class>com.tcs.tool.HTTPAccessFilter</filter-class>
467 <init-param>
468 <param-name>server.time.log.path</param-name>
469 <param-value>C:/logs/HTTPserverAccess.txt</param-value>
470 </init-param>
471 <init-param>
472 <param-name>browser.time.log.enabled</param-name>
473 <param-value>true</param-value>
474 </init-param>
475 <init-param>
476 <param-name>browser.time.log.path</param-name>
477 <param-value>C:/logs/HTTPClientSideAccess.txt</param-value>
478 </init-param>
479</filter>
480</code>
481</pre>
482
483<p>The <code>init</code> 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 <code>filter-mapping</code> -- that is, the URL patterns or servlet requests that should be intercepted by this <code>Filter</code> -- in web.xml.</p>
484
485<p>In the example as you've seen it so far, the <code>FileWriter</code> 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 <code>Filter</code> code and to web.xml.</p>
486
487<h2>Inserting the instrumentation scripts in the Web page</h2>
488
489<p>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. However, there are two other options you can explore:</p>
490
491<ul>
492<li><p>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.</p></li>
493
494<li><p>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.</p></li>
495</ul>
496
497<p>You will see how to take the second approach in this article. 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.</p>
498
499<p>In the source supplied with 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.</p>
500
501<p>You need to modify the HTTP content of the HTML-based response and insert the line in Listing 10 before the <code></head></code> tag.</p>
502
503<h4>Listing 10. JavaScript snippet to be inserted in the head section of the Web page</h4>
504
505<pre>
506<code>
507<script language='JavaScript' src='/YourContextRoot/timecapturejs/client_time_capture.js' type='text/javascript'></script>
508</code>
509</pre>
510
511<p>You also need to insert the content in Listing 11 before the <code></body></code> tag. That way, when the page is loaded, the <code>addLoadEvent</code> JavaScript function is called.</p>
512
513<h4>Listing 11. JavaScript snippet to be inserted just before the </body> tag of the Web page</h4>
514
515<pre>
516<code>
517<script language='JavaScript'>
518addLoadEvent();
519</script>
520</code>
521</pre>
522
523<p>Now take a look at Listing 12, which is a snippet of the new <code>Filter</code>.</p>
524
525<h4>Listing 12. Portion of the new Filter</h4>
526
527<pre>
528<code>
529public class ScriptInjectionFilter implements Filter {
530
531
532//all required other methods
533//...
534//...
535
536 public void doFilter(final ServletRequest request,
537 final ServletResponse response,
538 final FilterChain chain) throws IOException, ServletException {
539
540 //..
541 HttpServletRequest httpRequest = (HttpServletRequest) request;
542 CharResponseWrapper wrappedResponse =
543 new CharResponseWrapper((HttpServletResponse) response);
544 try {
545 chain.doFilter(request, wrappedResponse);
546 byte[] bArray = wrappedResponse.getData();
547 if ( bArray != null && bArray.length > 0 &&
548 getShouldInject(wrappedResponse) ) {
549 bArray = modifyContent(bArray,httpRequest);
550 }
551 if ( bArray != null ) {
552 response.getOutputStream().write(bArray);
553 response.getOutputStream().close();
554 }
555 } catch (IOException ioe) {
556 throw ioe;
557 } catch (ServletException se) {
558 throw se;
559 } catch (RuntimeException rte) {
560 throw rte;
561 }
562 }
563 private boolean getShouldInject(ServletResponse response) {
564 // does the contentType allow HTML injection?
565 String contentType = response.getContentType();
566 String strContentType =
567 (contentType != null) ? contentType.toLowerCase() : "";
568 if ((strContentType.indexOf("text/html") == -1) &&
569 (strContentType.indexOf("application/xhtml+xml") == -1)) {
570 // don't inject anything if the content is not html
571 return false;
572 }
573 return true;
574 }
575
576 private static final String startScript1 = "<script language='JavaScript' src='";
577 private static final String endScript1 =
578 "/timecapturejs/client_time_capture.js' type='text/javascript'></script>\n";
579 private static final String startScript2 =
580 "<script language='JavaScript'>\naddLoadEvent();\n</script>\n";
581
582 private byte[] modifyContent(byte[] array, HttpServletRequest httpRequest) {
583 String sContent = new String(array);
584 StringBuffer sbf = new StringBuffer(sContent);
585 sContent = sContent.toLowerCase();
586 int indexOfEndHead = sContent.indexOf("</head>");
587 if ( indexOfEndHead != -1 ) {
588 sbf = sbf.insert(indexOfEndHead, startScript1 +
589 httpRequest.getContextPath() + endScript1);
590 int indexOfEndBody =
591 sbf.toString().toLowerCase().indexOf("</body>");
592 if ( indexOfEndBody != -1 ) {
593 sbf = sbf.insert(indexOfEndBody, startScript2);
594 return sbf.toString().getBytes();
595 }
596 else {
597 return array;
598 }
599 }
600 else {
601 return array;
602 }
603 }
604}
605</code>
606</pre>
607
608<p>In <code>ScriptInjectionFilter</code>'s <code>doFilter()</code> method, you are wrapping the <code>HttpServletResponse</code> object in a class named <code>CharResponseWrapper</code>. <code>CharResponseWrapper</code>'s code is supplied in the article's sample code. <code>CharResponseWrapper</code> extends <code>HttpServletResponseWrapper</code>. The main purpose of this wrapper is to get the HTTP response content written in a temporary character array, instead of using the <code>PrintWriter</code>'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.</p>
609
610<p>After wrapping the response, you call <code>FilterChain</code>'s <code>doFilter()</code> method. This method executes the actual request; afterwards, you get the value using <code>wrappedResponse.getData()</code>. If the size of the content is greater than 0 the content is HTML, then you modify the content by calling the <code>modifyContent()</code> method. Inside <code>modifyContent()</code>, you search for the <code></head></code> tag and insert the reference to the client_time_capture.js file before that tag.</p>
611
612<p>Next, you search for the <code></body></code> tag and insert the <code>addLoadEvent</code> JavaScript function call. Finally, in the <code>doFilter()</code> method, you write the modified content in <code>HTTPServletResponse</code>'s output stream.</p>
613
614<p>Listing 13 shows the entry for this <code>Filter</code> that you'll have to add to web.xml.</p>
615
616<h4>Listing 13. web.xml entry for ScriptInjectionFilter</h4>
617
618<pre>
619<code>
620<filter>
621 <filter-name>ScriptInjectionFilter</filter-name>
622 <filter-class>com.tcs.tool.filter.ScriptInjectionFilter</filter-class>
623</filter>
624<filter-mapping>
625 <filter-name>ScriptInjectionFilter</filter-name>
626 <url-pattern>*.jsp</url-pattern>
627</filter-mapping>
628</code>
629</pre>
630
631<p>The <code>filter-mapping</code> 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 <code>filter-mapping</code> block with the <code>url-pattern</code> *.do. Alternately, you can give the Struts Action servlet in the <code><servlet-name></code> parameter under <code>filter-mapping</code>. Similarly, if you're dealing with a JSF application, you might have to map *.faces or *.jsf, or the Faces servlet itself, in the <code>filter-mapping</code>.</p>
632
633<h2>Getting more complex</h2>
634
635<p>The sample that's presented so far offers a reasonably complete look at one way to measure application response time from the client's perspective. But as I've already noted briefly, the techniques outlined won't cover every situation. In the rest of this article, you'll see how you might adapt the ideas discussed in this article in trickier contexts.</p>
636
637<h3>Ajax-based calls</h3>
638
639<p>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 <code>XMLHttpRequest</code> to be a real JavaScript object -- Internet Explorer 6 treats it an ActiveX object, for instance. Therefore, it is difficult to attach functions like <code>onopen</code> and <code>onsend</code> to intercept the <code>open</code> and <code>send</code> methods of <code>XMLHttpRequest</code>. The solution is to wrap the original <code>XMLHttpRequest</code> object with a real JavaScript object that supports the interception of the <code>XMLHttpRequest</code> object's operation. You can use <a href="#resources">the open source xmlhttprequest project</a> to do just that. Once you wrap the <code>XMLHttpRequest</code> in a proper JavaScript object, then you can intercept method calls like <code>open</code> and <code>send</code> to inject your time-capturing code just before the <code>send</code> method is called. Listing 14 shows how it's done.</p>
640
641<h4>Listing 14. Intercepting the Ajax send function</h4>
642
643<pre>
644<code>
645XMLHttpRequest.prototype.originalSend = XMLHttpRequest.prototype.send;
646XMLHttpRequest.prototype.send = captureTimeAjaxSend;
647
648function captureTimeAjaxSend(vData) {
649 createCookie('pagepostTime', getDateString() );
650 this.originalSend(vData);
651}
652</code>
653</pre>
654
655<p>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.)</p>
656
657<h4>Listing 15. Modifying XMLHttpRequest.src.js to intercept the Ajax return call</h4>
658
659<pre>
660<code>
661if (oRequest.readyState == cXMLHttpRequest.DONE) {
662 try {
663 captureTimeAjaxReturn(oRequest);
664 }catch(e){};
665}
666</code>
667</pre>
668
669<p>The code in Listing 15 checks to see if the <code>readyState</code> of the wrapped <code>XMLHttpRequest</code> object is <code>DONE</code> or not (state 4). If the request is completed, then it calls <code>captureTimeAjaxReturn</code>, and the <code>XMLHttpRequest</code> is passed. In Listing 16, you can note down the load time. The <code>captureTimeAjaxReturn</code> method will be called after the developer's function (which has been registered using <code>onreadystatehandler</code>). </p>
670
671<h4>Listing 16. Capturing t4 for the Ajax return call</h4>
672
673<pre>
674<code>
675function captureTimeAjaxReturn(xmlHttp) {
676 //capture the load time
677}
678</code>
679</pre>
680
681<p>Remember that you must explicitly include the reference to XMLHttpRequest.src.js in your pages. The script insertion <code>Filter</code> supplied with the sample code does not inject this script.</p>
682
683<h3>Alternate approaches to capturing t0</h3>
684
685<p>Earlier, you saw how to capture t0 for most server-side initiations by overriding the <code>onbeforeunload</code> function. Another possibility would be to intercept different points, like submit events or link clicks. Let's consider these two possibilities in turn.</p>
686
687<h4>Submit events</h4>
688
689<p>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 <code>submit</code> attribute, or as a JavaScript submit method on a form object. The two types of submit events need to be intercepted in different ways.</p>
690
691<p>A user-initiated auto submit might happen when the user clicks on a form with one of the attributes shown in Listing 17.</p>
692
693<h4>Listing 17. User-initiated auto submit examples</h4>
694
695<pre>
696<code>
697<input type="submit" .../>
698<input type="image" .../>
699</code>
700</pre>
701
702<p>To capture user-initiated auto submit, you must attach an <code>onsubmit</code> JavaScript event to every form on your Web page, as shown in Listing 18.</p>
703
704<h4>Listing 18. Capturing submit events for user-initiated auto submit</h4>
705
706<pre>
707<code>
708function captureSubmit() {
709 try {
710 var allForms = document.forms;
711 if ( allForms != null ) {
712 for ( var i=0; i < allForms.length; i++) {
713 allForms[i].onsubmit = capturePostTimeForSubmit;
714 }
715 }
716 }catch(e){}
717 return true;
718}
719</code>
720</pre>
721
722<p>Listing 18 loops through all the forms in the document and registers a custom function (<code>capturePostTimeForSubmit</code>) with an <code>onsubmit</code> event to each of them. Inside the <code>capturePostTimeForSubmit</code> method, you get the current time and write it inside the <code>pagepostTime</code> cookie, as shown in Listing 19.</p>
723
724<h4>Listing 19. Capturing t0 for user-initiated auto submit</h4>
725
726<pre>
727<code>
728function capturePostTimeForSubmit(event) {
729 createCookie('pagepostTime', getDateString());
730}
731</code>
732</pre>
733
734<p>As noted, you may also need to capture JavaScript submits. There's a problem, though: if you register the <code>onsubmit</code> handler with a form that uses a JavaScript submit event, the handler will not be called when the <code>submit</code> function executes. Therefore, in such a case you'd need to overwrite the <code>submit</code> function itself instead of attaching the <code>onsubmit</code> handler. Listing 20 shows how you'd do it.</p>
735
736<h4>Listing 20. Capturing the JavaScript submit function call</h4>
737
738<pre>
739<code>
740function captureJavaScriptSubmit() {
741 for (var form, i = 0; (form = document.forms[i]); ++i) {
742 form.realSubmit = form.submit
743 form.submit = function () {
744 if ( capturePostTimeForJavaScriptSubmit(this) )
745 this.realSubmit();
746 }
747 }
748}
749</code>
750</pre>
751
752<p>In Listing 20, you first loop through all the forms in the document, first storing the actual <code>submit</code> method in <code>form.realSubmit</code>. Then you override the original <code>submit</code> method and attach anonymous JavaScript to it. Inside the anonymous function, you call the <code>capturePostTimeForJavaScriptSubmit</code> method; inside <em>that</em> method, you capture the current time and write it inside the cookie, as shown in Listing 21.</p>
753
754<h4>Listing 21. Capturing t0 for the JavaScript submit function call</h4>
755
756<pre>
757<code>
758function capturePostTimeForJavaScriptSubmit(aForm) {
759 createCookie('pagepostTime', getDateString() );
760}
761</code>
762</pre>
763
764<p>You may wonder why you're using <code>capturePostTimeForJavaScriptSubmit</code> instead of <code>capturePostTimeForSubmit</code>. You <em>could</em> 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 <code>action</code> that are being fired inside the <code>capturePostTimeForSubmit</code> and <code>capturePostTimeForJavaScriptSubmit</code> methods, and you can send this information to the server. As the object types are different, so the procedure for extracting the <code>action</code> attribute of the form is also different. Hence, to separate the process, you need both methods.</p>
765
766<p>For example, in <code>capturePostTimeForSubmit</code>, you can extract the <code>action</code> attribute of the form as in Listing 22.</p>
767
768<h4>Listing 22. Extracting the action attribute for user-initiated auto submit</h4>
769
770<pre>
771<code>
772function capturePostTimeForSubmit(event) {
773 var target = event ? event.target : this;
774 var actionValue = "UNKNOWN";
775 if ( target.action != null && target.action != "undefined") {
776 actionValue = target.action;
777 //Now you can write the action in a cookie and send it to server to track
778 }
779 //old code
780 createCookie('pagepostTime', getDateString());
781}
782</code>
783</pre>
784
785<p>The equivalent for a JavaScript submit is illustrated in Listing 23.</p>
786
787<h4>Listing 23. Extracting the action attribute for JavaScript submit</h4>
788
789<pre>
790<code>
791function capturePostTimeForJavaScriptSubmit(aForm) {
792 var actionValue = "UNKNOWN";
793 if ( aForm.action != null && aForm.action != "undefined") {
794 actionValue = aForm.action;
795 //Now you can write the action in a cookie and send it to server to track
796 }
797 //old code
798 createCookie('pagepostTime', getDateString());
799 return true;
800}
801</code>
802</pre>
803
804<p>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.</p>
805
806<p>As in the earlier scenario, the <code>captureJavaScriptSubmit</code> function needs to be called at the end of the page.</p>
807
808<p>Before moving on, I want to note some interesting behavior I've noticed when using these techniques with submit events in an application built with the Apache MyFaces implementation of JavaServer Faces. In the application, if I used an <code>h:commandLink</code> 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 <code>onsubmit</code> handler is attached to the form being submitted. If an <code>onsubmit</code> event is attached, the generated JavaScript calls the <code>onsubmit</code> function associated with it. As a result, both <code>capturePostTimeForSubmit</code> and <code>capturePostTimeForJavaScriptSubmit</code> were called before submit -- <code>capturePostTimeForSubmit</code> was called for the <code>onsubmit</code> handler that I attached, and <code>capturePostTimeForJavaScriptSubmit</code> 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 <code>pagepostTime</code> cookie is written twice before the submit.</p>
809
810<h4>Capturing link clicks</h4>
811
812<p>Capturing link clicks is little bit tricky. Consider the following examples:</p>
813
814<ul>
815<li><p><code><a href="http://myserver/CTXRoot/Test.jsp">just a link (no onclick)</a></code>: Just a plain link. Clicking on it takes the user to another Web page.</p></li>
816
817
818<li><p><code><a href="http://myserver/CTXRoot/Test.jsp" onclick="return fromLink('Hello')">just a link (with onclick)</a></code>: An additional JavaScript function, <code>fromLink</code>, is attached to the link. The <code>fromLink</code> function is a developer-defined function that executes some logic and returns a boolean value. If the return value is <code>true</code>, the URL mentioned in the <code>href</code> attribute, http://myserver/CTXRoot/Test.jsp, is opened. If the return value is <code>false</code>, then nothing happens. Hence, you only need to capture t0 if the return value of the function is <code>true</code>.</p></li>
819
820
821<li><p><code><a href="javascript:alert(1)">just a link (href contains javascript)</a></code>: The <code>href</code> attribute contains only JavaScript. Here, you should not capture t0 under any circumstance.</p></li>
822
823
824<li><p><code><a href="#" onclick="someMethod()">just a link (href contains #, onclick contains a JavaScript method)</a></code>: Here the <code>href</code> attribute contains a value of <code>"#"</code>. You should also not capture t0 in this case. Recall from the discussion of the JavaScript method mentioned in regards to <code>onclick</code> that you can submit a form using the <code>form.submit</code> method; in that case, t0 will automatically be captured by the JavaScript submit handling.
825</p></li>
826</ul>
827
828
829<p>Keeping these scenarios in mind, look at the JavaScript functions in Listing 24.</p>
830
831<h4>Listing 24. Capturing t0 for various link click scenarios</h4>
832
833<pre>
834<code>
835 function captureLinkClicks() {
83601: try {
83702: var links = document.getElementsByTagName('a');
83803: for (var i = 0; i < links.length; i++) {
83904: var hrefString = links[i].href + "";
84005: if ( hrefString.indexOf("#") == -1 && hrefString !== ""
84106: && !hrefContainsScript(hrefString) ) {
84207: if ( links[i].onclick ) {
84308: links[i].oldonclick = links[i].onclick;
84409: }
84510: links[i].onclick = function() {
84611: var ret = false;
84712: if (this.oldonclick) {
84813: ret = this.oldonclick();
84914: if ( false != ret ) {
85015: capturePostTimeForLink(this);
85116: }
85217: return ret;
85318: }
85419: else {
85520: capturePostTimeForLink(this);
85621: return true;
85722: }
85823: }
85924: }
86025: }
86126: }catch(e){}
86227: return true;
863 }
864function hrefContainsScript(hrefString) {
865 hrefString = hrefString.toUpperCase();
866 hrefString = trim(hrefString);
867 if ( hrefString.substring(0,10) == "JAVASCRIPT") {
868 return true;
869 }
870 return false;
871}
872function trim(str) {
873 var a = str.replace(/^\s+/, '');
874 return a.replace(/\s+$/, '');
875}
876function capturePostTimeForLink(aLink) {
877 createCookie('pagepostTime', getDateString() );
878}
879</code>
880</pre>
881
882<p>The <code>captureLinkClicks</code> 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 <code>href</code> attribute of the anchor tag contains <code>"#"</code>, then you should not capture t0. The same is true if the <code>href</code> attribute is set to a blank string, or if the <code>href</code> attribute contains JavaScript instead of a URL. Lines 04 and 05 determine whether any of those conditions are true.</p>
883
884<p>Now imagine that the <code>href</code> attribute of the anchor tag contains a URL. In that case, you'd need to check to see if any existing JavaScript <code>onclick</code> functions are already attached to the anchor tag. If an <code>onclick</code> event already exists, then you store the existing <code>onclick</code> event to another attribute, named <code>oldonclick</code>, in that anchor object itself. Next, you override the <code>onclick</code> function of the anchor object. If any <code>oldonclick</code> event is attached to the anchor object, you call that event.</p>
885
886<p>Be aware that the old <code>onclick</code> may look something like Listing 25.</p>
887
888<h4>Listing 25. onclick of a link containing multiple function calls</h4>
889
890<pre>
891<code>
892onclick="alert(1);alert(2);somefunc('some param')" </code>
893</pre>
894
895<p>In other words, <code>onclick</code> may not contain a single JavaScript function call. In this approach, all the JavaScript that is written inside the existing <code>onclick</code> will be executed when Line 12 from Listing 24 executes. Again, remember that Line 12 will not execute when <code>captureLinkClicks</code> is called. Here, you're actually declaring an anonymous function on the link's <code>onclick</code> event. When the user clicks the link, only Line 12 is executed.</p>
896
897<p>Now, if the <code>onclick</code> method returns <code>false</code>, then the browser does not fire the URL. Therefore, Line 13 checks to see if the return value of the <code>oldonclick</code> event is <code>false</code> or not. If the return value is not false, then you should capture t0. If there is no <code>onclick</code> already associated in the anchor tag, then you should capture t0 and return <code>true</code> from the anonymous JavaScript function (Lines 20 and 21 of Listing 24).</p>
898
899<p>The other helper functions, such as <code>hrefContainsScript</code> and <code>trim</code>, should be self-explanatory.</p>
900
901<h2>Conclusion, and caveats</h2>
902
903<p>In this article, I have suggested an approach to capturing the end-user response time for various scenarios, and tried to provide a few code snippets to prove that the approach works. However, in dealing with this article's sample code, you should keep in mind following points:</p>
904
905<ul>
906<li><p>I have tested the scripts in this article 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 browsers that I tested.</p></li>
907
908<li><p>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. Please also note that 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.</p></li>
909
910<li><p>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.</p></li>
911
912<li><p>You may have already guessed that the time for the first and last page loads cannot be captured using this approach.</p></li>
913
914<li><p>You can enhance the <code>ScriptInjectionFilter</code> and <code>HTTPAccessFilter</code> 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 (<code>ServletContext</code>), 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.</p></li>
915
916<li><p>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.</p></li>
917
918<li><p>The approach outlined here may not work properly if multiple frames or framesets are involved in a page.</p></li>
919
920<li><p>If the developer has already written an <code>onbeforeunload</code> 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.</p></li>
921
922<li><p>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.</p></li>
923
924</ul>
925
926<p>In most cases, developers only record server-side execution time and ignore actual end-user response time. Hopefully this article has shown you how to approach the recording of your application's actual execution time from the end user's perspective.</p>
927
928<p>This article has also shown you how to insert instrumentation JavaScript automatically onto all the Web pages in an application with minimal effort. You can use a similar approach to modify your HTTP responses for any other purpose.</p>
929
930
931<h2>About the author</h2>
932<p><a href="http://www.javaworld.com/feedback"target="_blank">Srijeeb Roy</a> 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 ten 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 several other areas, such as Forte, CORBA, and Java ME.</p>
933
934
935<a name="resources">
936<h2><a name="resources">Resources</a></h2></a>
937
938<ul>
939
940<li>Download the <a href="clientsidetimecapture-src.zip">source code</a> that accompanies this article.</li>
941
942<li><a href="http://jensor.sourceforge.net">Jensor</a> is an open source Java profiler.</li>
943
944<li>The <a href="http://code.google.com/p/xmlhttprequest">xmlhttprequest open source project</a> can turn an Ajax XMLHttpRequest object into a true JavaScript object. For more information, see "<a href="http://www.ilinsky.com/articles/XMLHttpRequest/">Re-inventing XMLHttpRequest: Cross-browser implementation with sniffing capabilities</a>" (Sergey Ilinsky, October 2007).</li>
945
946<li>Read other JavaWorld articles by Srijeeb Roy:
947
948<ul>
949
950<li>"<a href="http://www.javaworld.com/javaworld/jw-01-2006/jw-0130-proxy.html">Generically chain dynamic proxies</a>" (January 2006)</li>
951
952<li>"<a href="http://www.javaworld.com/javaworld/jw-04-2006/jw-0417-push.html">Push messages that automatically launch a Java mobile application</a>" (April 2006)</li>
953
954<li>"<a href="http://www.javaworld.com/javaworld/jw-02-2007/jw-02-jsf.html">Group radio buttons inside a JSF dataTable component</a>" (February 2007)</li>
955
956<li>"<a href="http://www.javaworld.com/javaworld/jw-09-2007/jw-09-mobilevideo1.html">Mobile video with JME and MMAPI, Part 1: Use the Mobile Media API to add video functionality to your Java mobile applications</a>" (September 2007)</li>
957
958<li>"<a href="http://www.javaworld.com/javaworld/jw-09-2007/jw-09-mobilevideo2.html">Mobile video with JME and MMAPI, Part 2: Send and receive video files in your Java mobile applications</a>" (September 2007)</li>
959</ul>
960
961<li>Visit the JavaWorld <a href="http://www.javaworld.com/channel_content/jw-enterprise-index.html">Java Enterprise Edition research center</a> for more articles about enterprise Java programming tools and concepts.</li>
962
963<li>Also check out the JavaWorld <a href="http://www.javaworld.com/community/?q=javaqa">developer forums</a> for discussions and Q&A.</li>
964
965</ul>
966
967</body>
968</html>
969