CoverStory
LANGUAGES: VB.NET
ASP.NET VERSIONS: 1.x
Web App Diagnostics
Lightweight ASP.NET Automated Performance Analysis
By Dr. James McCaffrey
The ASP.NET environment has the ability to record performance metrics for a Web application simply by enabling tracing. When tracing is enabled you can manually retrieve the performance information by requesting the virtual trace.axd page. But what if you want to automate performance analysis using this mechanism? In this article, I ll show you a simple system to do just that.
As the saying goes, a picture is worth a thousand words, and the best way to demonstrate where I m headed is with a screen shot. Figure 1 shows that I am analyzing the performance of a dummy Web application that searches a data store of product information. Users can search by product ID or product description. My lightweight performance system automatically launches an instance of Internet Explorer, simulates searching for products with the character i in the description, then retrieves the performance trace information for the total time required to render the application page.
Figure 1: An example test run.
Obviously real Web applications are much more complex, but the techniques I ll show you can be used to automatically analyze the performance of any ASP.NET Web application hosted in Internet Explorer. In the sections that follow, I ll briefly describe the dummy Web application under analysis, present and explain in detail the performance analysis code that generated the image in Figure 1, and describe how you can modify and enhance the techniques presented in this article to meet your own needs. You should find the information here a useful addition to your developer, tester, and manager skill sets.
The Web Application under Analysis
Let s take a short look at the Web application we re analyzing so you can understand the goal of the performance automation. Figure 2 shows the state of the Web app after a user selected Product Description from the Search By dropdown control, then typed i into the textbox control, and then clicked the Search button. The search returns product ID, description, and price for two matching products in a listbox control. If you look at the IE address bar you can see I accepted the default WebForm1.aspx name for the dummy application. For simplicity, I also accepted default control names of DropDownList1, TextBox1, Button1, and ListBox1. I will need to know the names of my controls for my automation code.
Figure 2: The Web application under analysis.
One of the advantages of the performance analysis system I m presenting here is that it does not require access to the application s source code. I used C# to code the Web application, but I could have used VB.NET or any other .NET-compliant language. As I mentioned in the previous section, I do need to know the names of the controls to be able to write my performance analysis automation. In cases where you are analyzing your own Web application s performance, you ll obviously have this information. But even if you don t have direct access to the application s source code, you can easily get the control names by simply doing a View | Source in Internet Explorer.
One of the nice innovations that ASP.NET introduced to Web developers is the ability to enable the automatic capture and logging of performance data. If I go to the web.config file associated with my Web application, all I have to do is change the enabled attribute of the trace element from the default false to true:
enabled="true" requestLimit="10" pageOutput="false" traceMode="SortByTime" localOnly="true" /> Now trace data is recorded for each response-request
roundtrip. To view this data manually, I simply request the virtual page
trace.axd and click on the View Details link for the request I want to see. Figure
3 shows the trace information for the first request (id=0). Trace.axd is not a physical file, it is an alias for an in-memory
DataSet object on the Web server that holds all the trace information for a
particular session. When you issue a request to
http://machine/webroot/trace.axd, the request is intercepted, trace data is
pulled from the Dataset, and the data is placed in a dynamically created page
and returned to the client (as shown in Figure 3). This manual process of
examining trace data is fine for many situations, but what if you want to
automate the process? You ll need the ability to programmatically launch
Internet Explorer, load the Web application under analysis, simulate manipulating
the application, and retrieve the trace data. The performance analysis system consists of a single file.
I decided to implement my harness as a VB.NET console application program, but
I could have used C# or any other .NET-conformable language. The overall
structure of the program is shown in Figure 4. After creating a new project
using Visual Studio.NET, I add a project reference to the Microsoft Internet
Controls component from the classic COM component list. This will allow me to
create an InternetExplorer object. Next, I add a reference to the .NET
component System.Net so I can create a WebClient object. Imports System.Diagnostics Imports System.Threading Imports System.Net Imports SHDocVw Imports System.IO Module Module1 Dim docComplete As New
AutoResetEvent(False) Friend WithEvents ie As
SHDocVw.InternetExplorer Sub Main() Try ' Launch an IE
process ' Connect an IE
object to process ' Clear in-memory
trace cache ' Simulate
manipulating Web app docComplete.WaitOne(10000, True) ' Close IE ' Retrieve
performance data Catch ex As Exception Console.WriteLine("Fatal error " & ex.Message) End Try End Sub Private Sub
ie_docComplete(ByVal sender As System.Object, _ ByRef url As System.Object) _ Handles ie.DocumentComplete docComplete.Set() End Sub End Module Figure 4: The performance
analysis program structure. Next, I add several Imports statements so I don t have to
fully qualify classes contained in their namespaces. I add an import to
System.Diagnostics so I can access its Process class, and add an import to
System.Threading so I can call its Thread.Sleep method and create an
AutoResetEvent to signal when my Web app is fully loaded into IE. I also add an
import to SHDocVw, which is the namespace name associated with the Microsoft
Internet Controls component. Finally, I add an import to System.IO so I can
create a Stream object to capture the response to the request for performance
data on the Web server. One of the trickiest parts of automating Web applications
is determining exactly when a Web page is fully loaded in IE. To do this, I
declare a module-scope AutoResetEvent object named docComplete that I ll use to
notify a waiting thread that a document (Web application) is fully loaded: Dim docComplete As New AutoResetEvent(False) I ll explain this idea in more detail in a moment. Next, I
declare an InternetExplorer object and name it ie : Friend WithEvents ie As SHDocVw.InternetExplorer Notice that I use the WithEvents modifier so I can access
the InternetExplorer object s DocumentComplete event. I begin my performance
analysis by printing a start message to the command shell and declaring two
objects: Console.WriteLine(vbCrLf & "Start peformance analysis"
& vbCrLf) Dim p As New System.Diagnostics.Process Dim browsers As New SHDocVw.ShellWindows The Process object p is my actual Internet Explorer
process and the browsers object is a collection of all running browsers,
which includes Windows Explorer browsers and Internet Explorer browsers. Next,
I launch an instance of Internet Explorer: Console.WriteLine("Launching IE process") p = Process.Start("iexplore.exe", "about:blank") If p Is Nothing Then Throw New
Exception("Failed to launch IE") End If Console.WriteLine("IE process handle = " _ &
p.MainWindowHandle.ToString) Console.WriteLine("Number open browsers = " _ &
browsers.Count & " browsers") Notice that I launch IE and load the null about:blank
page. I capture the IE process information into my Process object p ; I ll use
this later to determine which active browser is my newly launched IE object: Console.WriteLine("Connecting IE object to IE process") Dim i As Integer Dim someIE As SHDocVw.InternetExplorer While i < browsers.Count And ie Is Nothing someIE = browsers.Item(i) If someIE.HWND.ToString()
= _ p.MainWindowHandle.ToString()
Then ie = someIE End If i = i + 1 End While I iterate through all the active browser objects looking
for a handle match. Because the InternetExplorer HWND (handle to window)
property is an integer, but the process handle is a System.IntPtr data type, I
convert both to strings so I can easily compare them for equality. My loop
terminates when I run out of browser objects to examine or when I find a match.
As a general rule, it s important to add error-checking to almost every step in
any automation: If ie Is Nothing Or p.MainWindowHandle.ToString() _ <>
ie.HWND.ToString() Then Throw New
Exception("Failed to attach to process correctly") Else Console.WriteLine("Successfully attached to process") End If I then make sure that my IE object attached to a browser
object and that it attached my newly created Internet Explorer instance. My
next step isn t completely necessary, but it s a nice touch: Console.WriteLine(vbCrLf & "Setting IE to 350 x
460") ie.Height = 350 ie.Width = 460 I adjust the InternetExplorer object s Height and Width
properties to a fixed size. Because I m measuring performance and the total
time for my Web app to load and render, I want to make sure the IE client area
is the same size for each analysis. Next I clear the trace cache: Console.WriteLine(vbCrLf & "Clearing trace cache") Dim zilch As New Object ie.Navigate("http://localhost/WebPerfApp/trace.axd?clear=1",
_ zilch, zilch,
zilch, zilch) docComplete.WaitOne(10000, True) To manually clear the trace cache, navigate to the virtual
trace.axd page and click on a clear current trace link. That link simply
sends a clear=1 query string to the Web server. So, to automate clearing the
trace cache I can send the same query string. The Navigate method of the
InternetExplorer object has a lot of parameters that I m not interested in so I
just pass empty objects as arguments to those parameters. I will explain the
WaitOne method call shortly. Next comes the key code to simulate a user
manipulating the Web app: Console.WriteLine("Simulating 'Product Description =
i'") ie.Navigate("http://localhost/WebPerfApp/WebForm1.aspx? _ __VIEWSTATE=dDwzOTI1MjUyNzQ7Oz7jkzW4GxDFbmU/
_ fHE0xnFD2fJxAA==&TextBox1=i&DropDownList1=
_ Product%20Description&Button1=clicked",
_ zilch, zilch, zilch, zilch) docComplete.WaitOne(10000, True) I append a query string with a ViewState value, and values
for the TextBox1, DropDownList1, and Button1 control values. Let me explain
each. The __VIEWSTATE (note: two underscores) is a Base64-encoded string that
represents the state of the Web application when the server last processed it. The
value I m using here is the value for the application when it is first loaded. It
is possible to get the ViewState value programmatically, but in this example I
found it simply by launching the application and then immediately doing a View
| Source in IE. If you try this code on your system you ll probably have to
change the ViewState value. The TextBox1 and DropDownList1 parts of the query
string are fairly self-explanatory. Notice I use %20 to represent a blank space
in Product Description . The Button1 value is a bit tricky. It turns out the
Web server will interpret any value for Button1 as meaning clicked , so I
could have written TextBox1=foo or anything else, but clicked is more
readable. Now let me explain how I pause my test automation until
the Web application document is completely loaded into IE. Recall that I
declared an AutoResetEvent named docComplete at the top of my Module. I also
wrote a subroutine that handles the InternetExplorer object s DocumentComplete
event: Private Sub ie_docComplete(ByVal sender As System.Object, _ ByRef url As System.Object) _ Handles ie.DocumentComplete docComplete.Set() End Sub When this subroutine is called it invokes the docComplete
object s Set method. In short, when I call the WaitOne method, my automation
will halt until the Web application is fully loaded, which fires the
InternetExplorer Document Complete event, which in turn will invoke the
docComplete.Set method, which will signal the automation to restart. The 10000
argument to WaitOne says to timeout after 10,000 milliseconds, or 10 seconds,
so I don t wait forever. After I ve exercised the Web application I can close
it: Console.WriteLine("Closing IE in 2 seconds") Thread.Sleep(2000) ie.Quit() This is very easy because the InternetExplorer object has
a Quit method. I put in a Thread.Sleep(2000) call to arbitrarily pause for two
seconds. Because this pauses my automation and not IE, I don t affect my
performance metrics. Now that I ve exercised the Web application under
analysis, I can programmatically retrieve the trace performance data. First I
set up my variables and objects: Dim webClient As New WebClient Dim s As Stream = _ webClient.OpenRead("http://localhost/WebPerfApp/
_ trace.axd?id=0") Dim sr As New StreamReader(s) Dim response As String response = sr.ReadToEnd() My strategy is to send a Web request to the virtual
trace.axd page and grab the HTTP response. I create a new WebClient object, and
a Stream object and a StreamReader object capture the HTTP response. I request
trace data, then grab the entire response stream into a single string variable
I named response . The next step is to parse out the information I need from
the response string: Dim target, pattern, time As String pattern = "End Render target = response.Substring(response.IndexOf(pattern),_ pattern.Length + 8) time = target.Substring(pattern.Length, 8) The trace result has a lot of information that you can
examine. In this case, I m looking for the cumulative time from the beginning
of application initialization to the end of page rendering. By examining the
complete response stream, I know that the time to end render is contained in
the HTTP response:
Figure 3: Manually viewing trace data. The Automation Code
"
End Render x.xxxxxx
I use the String.Subtring method to fetch a substring beginning with End Render and then use Substring again to extract only the eight characters that represent the total time value. Depending on what you re looking for, you ll have to edit this part of the program. All that s left to do is to tidy up and print my result:
sr.Close()
s.Close()
Console.WriteLine(vbCrLf & "Total time to
End Render = " & time)
Console.WriteLine(vbCrLf & "Done")
I close my open stream resources and print my total elapsed time to the shell. You may want to write results to a text file, SQL database, or other data store.
Adapting and Extending the Automation
You can extend and adapt the lightweight Web application performance analysis automation technique I ve presented here in several ways. Because the performance analysis system is an .exe file, you can easily schedule it to run without manual interaction, using the Windows Task Scheduler for example, or you can integrate the system into an automated build process.
For simplicity I used only minimal error checking. In a production system you ll want to insert additional exception handling try-catch-finally blocks and traditional error-checking statements. Also, the performance analysis system I ve presented has all arguments hard-coded for clarity. Depending on your environment, you may want to parameterize the program to make it more flexible.
You must be careful how you use performance analysis tools and interpret their results. Because the absolute performance of a Web application will depend on many user factors that are outside your control, you cannot use performance data to conclude exactly how users will experience your application. The best use of performance data is relative meaning, for example, that you can measure performance characteristics of your Web application and see how changes in your design affect performance.
With ASP.NET systems growing in complexity, analyzing the performance of your applications is more important than ever before. The lightweight automated performance analysis technique I ve described here can play an important role in your product s development effort.
The sample code referenced in this article is available for download.
Dr. James McCaffrey works for Volt Information Sciences, Inc., where he manages technical training for software engineers working at Microsoft s Redmond, WA campus. He has worked on several Microsoft products, including Internet Explorer and MSN Search. James can be reached at mailto:[email protected] or mailto:[email protected].