Profiling .NET Applications: Part 2
Using the Performance Monitor and PerformanceCounters to Profile .NET Applications
The .NET Framework has made it even easier to use and monitor performance counters. Even creating a custom performance counter is simple.
Bringing up the Performance Monitor and clicking on the Add button on the toolbar (the button with the '+' symbol on it) provides a list of the Performance Counters available (In Windows 2000, Performance Monitor can be found under Administrative Tools/Performance. In Windows XP, the Administrative Tools have been moved to the Control Panel, so you will have to bring up the Control Panel to open the Performance Monitor). Figure 1 shows the counters categories that have been added specifically to support .NET applications.
Figure 1 .NET performance counters.
As an example of using the .NET-specific performance counters, you will now see how to monitor JIT activity (Just-In-Time compilation, in case you have forgotten). The ThreadPoolTest application, presented in Chapter 14, has a way to control and visualize JIT activity. Figure 2 shows the Performance Monitor collecting data on an instance of ThreadPoolTest.
Figure 2 JIT counters.
As can be seen from Figure 2, two spikes indicated significant JIT activity. At the peak of each of these spikes, the total number of JITd bytes increased. Each of these spikes correspond to an invocation of a new instance of DiningPhilosophers.exe in the application ThreadPoolTest. It would be hard to devise another way of obtaining this kind of information without using performance counters. Many other .NET Framework-specific performance counters are available to provide this kind of specific information about your .NET application.
A list of the counters available is shown in Table 1.
Table 1.NET Performance Counters
Category |
Description / Counter |
Exception Performance Counters (.NET CLR Exceptions) |
Describes the performance counters that provide information about the exceptions thrown by an application. |
# of Exceps Thrown |
|
# of Exceps Thrown / Sec |
|
# of Filters / Sec |
|
# of Finallys / Sec |
|
Throw to catch depth / Sec |
|
Interop Performance Counters (.NET CLR Interop) |
Describes the performance counters that provide information about an application's interaction with COM components, COM+ services, and external type libraries. |
# of CCWs |
|
# of marshalling |
|
# of stubs |
|
JIT Performance Counters (.NET CLR JIT) |
Describes the performance counters that provide information about code that has been just-in-time (JIT) compiled. |
# of IL bytes JITted |
|
# of IL methods JITted |
|
% Time in JIT |
|
IL bytes JITted / sec |
|
Standard JIT failures |
|
Total # of IL bytes JITted |
|
Loading Performance Counters (.NET CLR Loading) |
Describes the performance counters that provide information about assemblies, classes, and application domains that are loaded. |
Bytes in Loader Heap |
|
Current AppDomains |
|
Current Assemblies |
|
Current classes loaded |
|
Rate of AppDomains |
|
Rate of AppDomains unloaded |
|
Rate of Assemblies |
|
Rate of classes loaded |
|
Rate of load failures |
|
Total # of load failures |
|
Total AppDomains |
|
Total AppDomains unloaded |
|
Total Assemblies |
|
Total classes loaded |
|
Lock and Thread Performance Counters (.NET CLR Locks AndThreads) |
Describes the performance counters that provide information about managed locks and threads that an application uses. |
# of current logical threads |
|
# of current physical threads |
|
# of current recognized threads |
|
# of total recognized threads |
|
Contention tate / sec |
|
Current queue length |
|
Queue length / sec |
|
Queue length peak |
|
Rate of recognized threads / sec |
|
Total # of contentions |
|
Memory Performance Counters (.NET CLR Memory) |
Describes the performance counters that provide information about the garbage collector. |
# Bytes in all heaps |
|
# GC handles |
|
# Gen 0 collections |
|
# Gen 1 collections |
|
# Gen 2 collections |
|
# Induced GC |
|
# of pinned objects |
|
# of sink blocks in use |
|
# Total committed bytes |
|
# Total reserved bytes |
|
% Time in GC |
|
Allocated bytes/second |
|
Finalization survivors |
|
Gen 0 heap size |
|
Gen 0 promoted rules/sec |
|
Gen 1 heap size |
|
Gen 1 promoted bytes/sec |
|
Gen 2 heap size |
|
Large object heap size |
|
PromotedFinalization - memory from Gen 0 |
|
Promoted finalization -memory from Gen 1 |
|
Promoted memory from Gen 0 |
|
Promoted memory from Gen 1 |
|
Networking Performance Counters (.NET CLR Networking) |
Describes the performance counters that provide information about data that an application sends and receives over the network. |
Bytes received |
|
Bytes sent |
|
Connections established |
|
Datagrams received |
|
Datagrams sent |
|
Remoting Performance Counters (.NET CLR Remoting) |
Describes the performance counters that provide information about the remoted objects that an application uses. |
Channels |
|
Context proxies |
|
Context bound Classes loaded |
|
Context bound Objects alloc / sec |
|
Contexts |
|
Remote calls / sec |
|
Total remote calls |
|
Security Performance Counters (.NET CLR Security) |
Describes the performance counters that provide information about the security checks that the common language runtime performs for an application. |
# Link time checks |
|
% Time in RT checks |
|
Stack walk depth |
|
Total runtime checks |
Samples have been put together to demonstrate the use of the performance counters described in Table 1. These samples are included in the source for this chapter. For the most part, each category of counter is illustrated by a single executable program along with an .msc file, which describes the counters to be used. You can double-click on the .msc file and double-click on the .exe file to see the performance counters in action. Some of the files will need to be edited to bring them in line with your environment (different server, different network adapter, and so on). When this is the case, a comment will be included on each of these categories. Most of the files can be built with something like csc filename.cs. For convenience, a batch file called build.bat has been included that compiles all of the performance counter samples.
The first category that I will look at is the .NET CLR Exceptions category. Listing 1 illustrates throwing exceptions every half-second (exception.cs).
Listing 1Throwing Exceptions
private void ThrowException() { throw new ApplicationException("Throwing an exception from ThrowException"); } . . . while(true) { try { ThrowException(); } catch(ApplicationException) { TimeSpan ts = new TimeSpan(Environment.TickCount); Console.WriteLine("{0}", ts.ToString()); } // Every half a second I throw an exception. Thread.Sleep(500); }
With this code, the Performance Monitor setup file (exception.msc) looks at two counters. The first, # of Exceps Thrown, steadily increases as the graph progresses because an unending number of exceptions are being thrown. The second counter is a rate, # of Exceps Thrown / sec. Because an exception is thrown once every half second, the rate remains constant at two exceptions per second. To stop the application, simply press Enter in the console window created for the application.
The next counter that fits into this category is # of Finallys / sec. Listing 2 illustrates a finally block (finally.cs).
Listing 2Returning from a Function Via Finally
private void FinallyFunction() { try { TimeSpan ts = new TimeSpan(Environment.TickCount); throw new ApplicationException(string.Format("{0} . . .", ts.ToString())); } finally { Console.WriteLine("finally"); } }
Loading the Performance Monitor file (finally.mcs) sets up only one counter. The results from this performance counter are much like the rate for the code associated with Listing 2. Here, the FinallyFunction is called every half second, and every time it is called, the finally block is guaranteed to be called. The result is a constant rate of two finallys per second.
Another category is .NET CLR JIT. Listing 3 shows the driver for the sample that drives the JIT Performance Counters (jit.cs).
Listing 3JIT in a Loop
while(true) { Type t = GenerateCode(); object o = Activator.CreateInstance(t); MethodInfo mi = t.GetMethod("HelloWorld"); mi.Invoke(o, null); Thread.Sleep(500); }
The Performance Monitor file (jit.mcs) loads five counters. When the sample is running, you should see the # of IL Bytes JITted, # of Methods JITted, and Total # of Bytes JITted steadily ramping up. This is because the loop creates a new method (albeit the same method) each time around the loop. Therefore, the entire method is jitted each trip around the loop. The last two counters, % Time in JIT and IL Bytes JITted / sec, are relatively constant because they are basically a rate, and the rate is throttled at a JIT every half second.
The next category, .NET CLR Loading, is best demonstrated with an application that has been introduced already, ThreadPoolTest.exe. This sample spawns multiple copies of the DiningPhilosophers sample, each in its own AppDomain. To run this test, double-click on loading.mcs and then run the ThreadPoolTest sample. Notice that each time you click to start a new instance of DiningPhilosophers, the number of AppDomains, classes, and assemblies increase. When you exit one of these instances, they decrease again.
The next category is .NET CLR LocksAndThreads. To demonstrate the logical, physical threads, and contention (locks), three samples have been included. Listing 4 shows an example of starting a series of 10 threads and then waiting for each to finish (pthread.cs).
Listing 4Starting Physical Threads
static void WorkerThread() { Thread.Sleep(r.Next(1000)); } static void TestThread() { try { while(true) { Thread [] t = new Thread[10]; for(int i = 0;i < 10;i++) { t[i] = new Thread(new ThreadStart(WorkerThread)); t[i].Start(); } for(int i = 0;i < 10;i++) { t[i].Join(); } Thread.Sleep(5000); } } catch(Exception e) { Console.WriteLine("TestThread aborting: {0}", e.Message); } }
The corresponding Performance Monitor file (pthread.msc) loads two counters: # of current logical Threads and # of current physical Threads. Notice that every five seconds, an additional 10 threads are started, and the logical and physical thread count are the same.
Next, look at the logical threads (lthread.cs). The thread pool is being used. Listing 5 shows what is being done to get a logical thread.
Listing 5Starting Threads from the Thread Pool
void WorkerThread(Object number) { int id = (int)number; // Sleep for 1 second to simulate doing work Thread.Sleep(1000); // Signal that the async operation is now complete. TimeSpan ts = new TimeSpan(Environment.TickCount); Console.WriteLine("{0} {1}", id, ts); asyncEvents[id].Set(); } void TestThread() { try { while(true) { Console.WriteLine("Start ----------------"); for(int i = 0;i < threadCount;i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(WorkerThread), i); } Console.WriteLine("End ----------------"); for(int i = 0;i < threadCount;i++) { asyncEvents[i].WaitOne(); asyncEvents[i].Reset(); } } } catch(Exception e) { Console.WriteLine("TestThread aborting: {0}", e.Message); } }
For this sample, the threadCount is 100. One hundred work-units have been allocated that might or might not happen simultaneously. Looking at the Performance Monitor (lthread.mcs), you can see a constant two physical threads and the number of logical threads, ramping up to 28 and then remaining constant. The thread pool is doing some work to keep the number of threads that are running at any given time down.
Contention (contention.cs) is also looked at, as shown in Listing 6.
Listing 6Starting Contending Threads
private void WorkerThread() { Monitor.Enter(this); Thread.Sleep(r.Next(1000)); Monitor.Exit(this); } private void TestThread() { try { while(true) { Thread [] t = new Thread[10]; for(int i = 0;i < 10;i++) { t[i] = new Thread(new ThreadStart(WorkerThread)); t[i].Start(); } for(int i = 0;i < 10;i++) { t[i].Join(); } Thread.Sleep(5000); } } catch(Exception e) { Console.WriteLine("TestThread aborting: {0}", e.Message); } }
Ten threads are started, but each has to wait while the thread ahead of it does some work (in this case, it sleeps for 1 second). Using several counters, this contention can be illustrated with the Performance Monitor (contention.mcs). This sample file monitors three counters (# of current logical Threads, # of current physical Threads, and Total # of Contentions). Notice that the count of threads allocated is steadily increasing, but the number of contentions is also steadily increasing.
Another Performance Monitor category is .NET CLR Memory. Two samples have been together to illustrate the counters in this category. One of the counters available is the number of pinned objects encountered when doing a garbage collection scan. The easiest way to illustrate this counter is to pin an object and initiate a garbage collection cycle. Listing 7 shows how to do this (gcpinned.cs).
Listing 7Forcing a Pinned Object
unsafe void PinnedThread() { byte [] ba1 = new byte[100]; byte [] ba2 = new byte[100]; byte [] ba3 = new byte[100]; byte [] ba4 = new byte[100]; for(int i = 0; i < ba1.Length; i++) { ba1[i] = (byte)(i % 256); ba2[i] = (byte)(i % 256); ba3[i] = (byte)(i % 256); ba4[i] = (byte)(i % 256); } fixed(byte *p1 = ba1,p2 = ba2, p3 = ba3, p4 = ba4) { asyncEvent.WaitOne(); } } void TestThread() { Thread t; asyncEvent = new ManualResetEvent(false); try { while(true) { asyncEvent.Reset(); t = new Thread(new ThreadStart(PinnedThread)); t.Start(); Thread.Sleep(500); // Performing a GC promotes the object's generation GC.Collect(); asyncEvent.Set(); t.Join(); Thread.Sleep(500); // Performing a GC promotes the object's generation GC.Collect(); Thread.Sleep(500); } } catch(Exception e) { Console.WriteLine("TestThread aborting: {0}", e.Message); } }
In the worker thread, an object is pinned and then you must wait for a signal to unpin the object(s). The resulting Performance Monitor output shows that a pinned object was encountered. The Performance Monitor setup loads one counter, # of Pinned Objects. This single counter now reads 1 when the code gcpinned.exe is run, indicating that one pinned object was encountered during GC.
Next, look at the generational garbage collector. Listing 8 shows how to move allocated objects through the generations of the garbage collector (gcgen.cs).
Listing 8Moving Through the Generations of the Garbage Collector (GC)
TestObj [] toa = new TestObj[10]; while(true) { for(int i = 0; i < toa.Length; i++) toa[i] = new TestObj("TestObj" + i.ToString()); // Performing a GC promotes the object's generation GC.Collect(); GC.Collect(); GC.Collect(); // Destroy the strong reference to this object for(int i = 0; i < toa.Length; i++) toa[i] = null; GC.Collect(0); GC.WaitForPendingFinalizers(); GC.Collect(1); GC.WaitForPendingFinalizers(); GC.Collect(2); GC.WaitForPendingFinalizers(); Thread.Sleep(500); }
Ten objects are allocated, and a GC is forced to collect cycle to move the objects from one generation to the next. The objects are set to null and collected, which frees memory associated with these objects. The associated gcgen.msc sets up counters to illustrate the collection as well as the induced collection (because a normal cycle would take a long time).
.NET CLR Networking is the next category to explore. You will probably need to do a little work to get this sample set up. Listing 9 shows some code to test a TCP connection.
Listing 9TCP Networking
Socket s = null; try { IPHostEntry host = Dns.GetHostByName(server); s = new Socket(host.AddressList[0].AddressFamily, SocketType.Stream, ProtocolType.Tcp); IPEndPoint remoteEP = new IPEndPoint(host.AddressList[0], port); s.Connect(remoteEP); Console.WriteLine("Connected to {0}", server); byte[] writeBuffer = new byte[16384]; byte[] readBuffer = new byte[16384]; int nTransmitted; int nReceived; int nTotalReceived; while(true) { nTransmitted = s.Send(writeBuffer, 0, writeBuffer.Length, SocketFlags.None); nReceived = 0; nTotalReceived = 0; while(nTotalReceived < nTransmitted) { nReceived = s.Receive(readBuffer, nReceived, nTransmitted - nTotalReceived, SocketFlags.None); nTotalReceived += nReceived; } } } catch(Exception e) { Console.WriteLine("TestThread aborting: {0}", e.Message); } finally { if(s != null) s.Close(); }
If you look at the source for this listing (tcp.cs), you will notice that it has a hardcoded name in it. You need to change that name to a server on your network that supports the echo protocol. This protocol echoes all of the bytes sent to it. Using this service makes it so that you don't have to set up a server process, which is much easier.
The second change you probably need to make is to the Performance Monitor setup file. You need to select a network adapter that corresponds to your system. To do this, just double-click on tcp.mcs and from the Performance Monitor application, change the settings and save the file back out to tcp.mcs (overwriting the old file). Now you will be able to see the number of bytes coming in (received) and the number of bytes going out (sent).
The next category is .NET CLR Remoting. The samples here might require some modification to fit your environment. The default configuration is for the client and the server to run from localhost. If that is okay, then you just need to bring up two command windows and run the server in one window and the client in the other. Make sure that you start the server first. Otherwise, you will generate an exception on the first method call on the client. Listing 10 shows how the server is implemented (remserver.cs).
Listing 10Remoting Server
public class Sample { public static int Main(string [] args) { TcpChannel chan = new TcpChannel(8085); ChannelServices.RegisterChannel(chan); RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType( "RemotingSamples.HelloServer,remobject"), "SayHello", WellKnownObjectMode.SingleCall); System.Console.WriteLine("Hit <enter> to exit..."); System.Console.ReadLine(); return 0; } }
Listing 11 shows the client (remclient.cs) for this remoting test sample.
Listing 11Remoting Client
public static int Main(string [] args) { try { TcpChannel chan = new TcpChannel(); ChannelServices.RegisterChannel(chan); HelloServer obj = (HelloServer)Activator.GetObject( typeof(RemotingSamples.HelloServer), "tcp://localhost:8085/SayHello"); while(true) { Console.WriteLine(obj.HelloMethod( "The client says hello.")); Thread.Sleep(500); } } catch(Exception e) { System.Console.WriteLine("Could not locate server"); Console.WriteLine(e); } return 0; }
Both the client and the server need to know about the object that is passed from the client to the server. Listing 12 shows this object (remobject.cs).
Listing 12Remoting Object
public class HelloServer : MarshalByRefObject { public HelloServer() { Console.WriteLine("HelloServer activated"); } public String HelloMethod(String name) { Console.WriteLine("Hello.HelloMethod : {0}", name); return "I received your greeting: " + name; } }
Two Performance Monitor setup files have been built: remserver.msc and remclient.msc. Both of these tell you the same thing, but you might want to monitor either the client or the server. A counter keeps track of the number of contexts. These counters should be a constant two. One counter, Remote Calls/sec, should be constant at 2 (2 per second or 1 every half-second) because of the Thread.Sleep(500) call. The last counter, Total Remote Calls, keeps track of the total number of remote calls made. This should be a steadily increasing ramp function, where the slope is the rate of remote calls, which is two per second.
Finally, a sample is provided to illustrate the security counters on the security category, .NET CLR Security. Not many counters are available, but building a sample that exercises these counters is difficult. To fully exercise these counters, you might need to edit the source for some of these tests. Five counters are listed. Three demonstrations are available that exercise most of these five counters. Listing 13 illustrates how the stack is walked to perform a security check (security.cs).
Listing 13Walking the Stack for Security
public static void CopyFile(String srcPath, String dstPath, bool stackCheck) { // Create a file permission set indicating all of this method's intentions. FileIOPermission fp = new FileIOPermission(FileIOPermissionAccess.Read, Path.GetFullPath(srcPath)); fp.AddPathList(FileIOPermissionAccess.Write | FileIOPermissionAccess.Append, Path.GetFullPath(dstPath)); // Verify that we can be granted all the permissions we'll need. fp.Demand(); // Assert the desired permissions here. if(!stackCheck) fp.Assert(); . . . } void RecurseSecurityCheck(int i) { if(i > 0) RecurseSecurityCheck(i - 1); else { // No stack walk // CopyFile(".\\Security.exe", ".\\Security.copy.exe", false); // Stack walk required CopyFile(".\\Security.exe", ".\\Security.copy.exe", true); } } void TestThread() { try { while(true) { RecurseSecurityCheck(50); Thread.Sleep(100); } } catch(Exception e) { Console.WriteLine("TestThread aborting: ", e.Message); } }
Notice that the RecurseSecurityCheck is called until the stack is of the size specified, and then the CopyFile routine is called. As shown in Listing 13, the stack check variable is true, so the fp.Assert() call is not executed. This means that the code will walk the stack to make sure that all contexts have the permission to execute the file operation. If fp.Assert() is allowed to be executed, then it turns off the stack walk and assumes that everyone in the call tree has the proper permission. The Performance Counters verify this. If you run the code as in Listing 13, then a counter is monitored called Stack Walk Depth, which has a steady value of 52. Fifty of those frames are due to the recursive call that is made before calling CopyFile. If the Assert is allowed to run, then the Stack Walk Depth is no longer 52, but 1.
Next, look at the counter, # Link Time Checks. To exercise this counter, modify the CopyFile routine used previously as (secattrib.cs):
[FileIOPermissionAttribute(SecurityAction.LinkDemand, Unrestricted=true)] public static void CopyFile(String srcPath, String dstPath)
The Performance Monitor (secattrib.mcs) shows the same numbers as before. It is still walking the stack, and a steadily increasing number of runtime checks are occurring, but the # Link Time Checks is 1. This is because when this routine was jitted, a security check was required by the attribute. If you change LinkDemand to Assert, no stack-walk will exist.
You might want to exercise the # Link Time Checks a little more. To do so, you can add the following lines to the jit.cs code:
PermissionSet ps = new PermissionSet(PermissionState.Unrestricted); EnvironmentPermission cp = new EnvironmentPermission (PermissionState.Unrestricted); ps.AddPermission(cp); methodBuilder.AddDeclarativeSecurity(SecurityAction.LinkDemand, ps);
This forces a security check each time a method is JITted. Because this code continually submits code to be JITted, you should see # Link Time Checks steadily increase (seclink.mcs).
Programmatic Access to the Performance Counters
It has always been somewhat of a chore to add performance monitoring to an application. In 1998, Jeffrey Richter wrote a detailed article on PerfMon.exe in the August 1998 issue of MSJ. In this article, he not only provided a good overview of the Performance Monitor application (PerfMon.exe), but he also showed how to add custom performance counters to your application. This information is still relevant and would be a good review of performance counters in general. You can find it at http://www.microsoft.com/msj/defaultframe.asp?page=/msj/0898/performance.htm.
Ken Knudsen proposed building a COM component to ease the task in the February 2000 issue of MSDN Magazine. You can read about it at http://www.microsoft.com/MSJ/0200/comperf/comperf.asp.
The Performance Monitor is an application that allows you to easily visualize performance counters. However, the Performance Monitor might not address specific requirements that must be met by your application. You might want to build a custom performance monitoring application, or you might want to incorporate certain performance counters into your application. The .NET Framework provides a powerful set of classes to support monitoring performance counters. A simple application can be found in the PerformanceMonitor directory. The application illustrates the usage of some of these classes. When the application starts, it looks like Figure 3.
Figure 3 Performance Monitor application.
This application lists performance counter categories that are available for the computer on which it is run. It also shows which instances and counters are associated with each category. Selecting a particular counter or instance starts a timer, which reads the value from the selected counter every second. How is this done?
NOTE
All of the performance monitor classes are in the System.Diagnostics namespace.
You need a list of the performance counter categories. GetCategories is a static function within the PerformanceCounterCategory class that retrieves the performance counter categories available on the local machine. An overloaded method takes a single string argument specifying the machine for which a category list is required. Using GetCategories is as simple as Listing 14.
Listing 14Getting a Listing of the Performance Counter Categories
PerformanceCounterCategory [] pcc = PerformanceCounterCategory.GetCategories(); foreach(PerformanceCounterCategory p in pcc) { categoryList.Items.Add(p.CategoryName);
In Listing 14, an array of the performance categories was retrieved, and the name of the category was added to a ListBox. Although getting the name of the categories is interesting, it is not the only method available within the PerformanceCounterCategory class. You also need to retrieve the instances associated with the category and the specific counters associated with the category. You can accomplish this in two ways.
The first method of drilling down into a performance counter category is the most efficient if you are dealing with whole categories at a time. It turns out that if you use the ReadCategory method, a snapshot is taken of the entire category. It is more efficient to read the category in bulk like this than to read each counter at a time. Calling the ReadCategory method returns an InstanceDataCollectionCollection. If you want to just list the counters associated with each category, you could form a loop like that shown in Listing 15.
Listing 15Getting a Listing of the Counters in Each Performance Counter Category
InstanceDataCollectionCollection dcc = p.ReadCategory(); ICollection keys = dcc.Keys; foreach(string key in keys) { Debug.WriteLine(string.Format("{0} {1}", p.CategoryName, key)); }
Listing 15 simply reads all data associated with the category and prints the category name and a counter associated with it. You need to find out about each instance associated with the counter and that data for the counter. Listing 16 shows how you can enumerate through each instance to get the data associated with the instance.
Listing 16Getting a Listing of the Instances and Data in Each Performance Counter Category
InstanceDataCollectionCollection dcc = p.ReadCategory(); . . . ICollection values = dcc.Values; foreach(InstanceDataCollection value in values) { Debug.Indent(); ICollection vvs = value.Values; foreach(InstanceData vv in vvs) { Debug.WriteLine(string.Format("{0} {1}", vv.InstanceName, vv.RawValue)); } Debug.Unindent(); }
An instance can be associated with each counter. For example, if you were looking at the thread counters, the name of the category for monitoring CLR threads would be .NET CLR LocksAndThreads. A counter in that category would be # of current physical Threads. Because each process can have many threads and multiple processes can exist, each process is recognized as an instance in performance counter lingo. Listing 16 shows how to move through each instance in the collection and prints the current value for that instance. In a real application, you would need to correlate the enumeration of the instance data with the counters that are part of the Keys, as shown in Listing 15. Also, note that not all counters are simply a count. Many types of counters exist, and a real application would need to handle each of the different kinds of counters. The following list presents some of the more common types of counters:
AverageTimer32This counter measures the time it takes on average to complete an operation or process. An average counter always has an AverageBase counter associated with it.
CounterTimerThis is a percentage counter that measures the average time that a component is active.
ElapsedTimeThis a difference counter that measures the total time between when a component of process was started and the current time.
NumberOfItems32This is an instantaneous counter that measures the most recently observed value.
RateOfCountsPerSecond32This is a difference counter that measures the average number of operations completed during each second of the sample interval.
SampleCounterThis is an average counter that measures the average number of operations completed in one second. When a counter of this type is sampled, a one or a zero is returned. The counter data is the number of ones that were sampled.
The sample application PerformanceMonitor doesn't use the methods described in Listings 15 and 16. This is primarily because for this application, you don't need to get the performance data from a whole category at once. This application used a different approach, as described in the following paragraph.
In the Performance Monitor application, when the user selects a performance category (initialized during startup), PerformanceCounterCategory is reconstructed and a method is called to get the instances associated with the category (GetInstanceNames). This method returns an array of strings that are associated with the category that is selected. This array of strings is used to fill the instance ListBox with the name of each instance. Not all categories have instances associated with them. If instances are not present, then you can construct an array of counters based on a single instance. In contrast, if multiple instances exist, try to construct an array of PerformanceCounters based on the first instance, assuming that the same number of counters exist for each instance. The code to do this is part of the PerformanceMonitor sample. A code snippet from this application is shown in Listing 17.
Listing 17Getting a Listing of the Instances and Data Associated with a Performance Counter Category
PerformanceCounterCategory pcc; if(computerList.Enabled) pcc = new PerformanceCounterCategory((string)cb.SelectedItem, (string)computerList.SelectedItem); else pcc = new PerformanceCounterCategory((string)cb.SelectedItem); string [] instanceNames = pcc.GetInstanceNames(); foreach(string s in instanceNames) { instanceList.Items.Add(s); } if(instanceNames.Length > 0) { // Get the counters for the last instance (they should all be the same) PerformanceCounter [] pca = pcc.GetCounters(instanceNames[0]); foreach(PerformanceCounter pc in pca) { counterList.Items.Add(pc.CounterName); } } else { PerformanceCounter [] pca = pcc.GetCounters(); foreach(PerformanceCounter pc in pca) { counterList.Items.Add(pc.CounterName); } }
After the user selects either a counter or an instance, a timer is started, which reads the PerformanceCounter every second. The code to accomplish this is shown in Listing 18.
Listing 18Obtaining a Current PerformanceCounter
PerformanceCounterCategory pcc = new PerformanceCounterCategory((string)categoryList.SelectedItem); PerformanceCounter pc; if(instanceList.SelectedIndex == -1 && instanceList.Items.Count > 0) { instanceList.SelectedIndex = 0; } if(computerList.Enabled) pc = new PerformanceCounter(pcc.CategoryName, (string)lb.SelectedItem, (string)instanceList.SelectedItem, (string)computerList.SelectedItem); else pc = new PerformanceCounter(pcc.CategoryName, (string)lb.SelectedItem, (string)instanceList.SelectedItem); counterHelp.Text = pc.CounterHelp; currentPerformanceCounter = pc; timer.Enabled = true;
This code creates a new PerformanceCounter based on the selections that the user has made for the category, counter, instance, and computer. The code that is executed when an instance is selected is similar. The last line of Listing 18 enables the timer. After this timer is enabled, it fires an event every second. The handler for this event looks at the current PerformanceCounter and reads various property values from the current instance. A portion of this code is shown in Listing 19.
Listing 19Reading the Current PerformanceCounter
ListViewItem item = new ListViewItem(currentPerformanceCounter.CounterName); CounterSample sample = currentPerformanceCounter.NextSample(); item.SubItems.Add(Convert.ToString(currentPerformanceCounter.CounterType)); item.SubItems.Add(Convert.ToString(sample.TimeStamp)); item.SubItems.Add(Convert.ToString(sample.RawValue)); samples.Items.Add(item);
This code creates an item to put into the ListView and populates the item with the name of the counter, the type of counter, a time stamp for the sample, and the raw value for the sample.
Adding a Custom Counter Category Using the Server Explorer
The Performance Monitor architecture is also extensible. You can add your own performance counter. The .NET Framework makes adding your own performance counter much easier. You can add a custom performance counter in two ways. The first method is to use the Server Explorer that is part of Visual Studio.NET. The second method is to add it programmatically, as discussed in the section "Adding a Counter Programmatically."
The Server Explorer tab of Visual Studio.NET brings new functionality to the developer's desktop. You can look at event logs, message queues, performance counters, services, and databases. The Performance Counter portion alone provides a list of the performance counter categories, performance counters, and performance counter instances. To create a new performance counter category, right-click on the Performance Counter node of the Server Explorer, and you will be presented with a pop-up menu that looks like Figure 4.
Figure 4 Adding a performance counter category by using the Server Explorer.
To demonstrate adding a custom performance counter, you can add a counter whose purpose is to count the number of times a "Hello World" method has been called.
To create a custom performance counter category, select the Create New Category menu item shown in Figure 4. After you have selected this menu item, you will be presented with a form similar to Figure 5.
Figure 5 New performance counter category form.
As you can see from Figure 5, the form is already partially filled in. The goal is to create a Hello World performance counter category that has just one performance counter in it called Count. You could add any number of counters to this category, but one counter is sufficient. After you click the OK button for this form, a new performance category is created, as shown in Figure 6.
Figure 6 The Hello World performance category.
Your new performance counter category joins the ranks of all of the other performance counter categories. You can do anything with this performance counter category that you can do with those categories that predated your own.
Building a Service to Write to the Custom PerformanceCounter
Now that you have a new performance counter, you need a program to modify it so that you can see it in action. To perform this action, you can use a service.
Building a service with Visual Studio.NET or even services in general have not been discussed yet. As you are probably well aware, a service is a process that can be configured to be manually or automatically started by the system. If the service is configured to automatically start, then the service will start every time the system reboots. If the service is configured to start manually, then the user can start or stop the service from the Server Explorer or the Services control snap-in.
Building a service is easy with Visual Studio.NET. A simple service solution has been included in the HelloWorldService directory. A new C# project has been started, and the Windows Service Wizard has been used to help build the service. Two components have been added: a performance counter, and a timer to modify the custom performance counter created in the previous section.
From the same "design" window where the timer and performance counter components were dragged, an "installer" has been added to the project by right-clicking on the Design window and selecting the Add Installer menu item. This adds a ProjectInstaller.cs file that has a design and code window. From the code window, you can see that two components are present: a ServiceProcessInstaller and a ServiceInstaller. You can set the account that the service will run under when started, or optionally, choose a username and password representing a user account. In this case, the service will run under the LocalSystem account. You can change the properties of the ServiceInstaller component to give the service a name and a startup type (among other properties). Here, the service is named HelloWorldService, and the startup type is Manual.
Now that you have a project installer as part of the project, you can build the project. The build will create a HelloWorldService.exe. From a command window, you can install the service using the installutil.exe utility that comes with the .NET Framework SDK. For the sample to work, you will need to install this service. From a command window, navigate to where the HelloWorldService.exe file has been built (either the Release or Debug subdirectory of bin in the project). At that point, install the service with installutil HelloWorldService.exe. If the service installs correctly, you should see something like Listing 20.
NOTE
Make sure that your environment variables are set correctly, or you will find that none of the preceding commands work. You can use a file called vcvars32.bat that is located in Microsoft Visual Studio .NET\Common7\Tools to set up your variables properly.
Listing 20Installing a Service
>installutil HelloWorldService.exe Microsoft (R) .NET Framework Installation utility Copyright (C) Microsoft Corp 2001. All rights reserved. Running a transacted installation. Beginning the Install phase of the installation. See the contents of the log file for the HelloWorldService.exe assembly's progress. The file is located at HelloWorldService.InstallLog. Call Installing. on the HelloWorldService.exe assembly. Affected parameters are: assemblypath = HelloWorldService.exe logfile = HelloWorldService.InstallLog Installing service HelloWorldService... Service HelloWorldService has been successfully installed. Creating EventLog source HelloWorldService in log Application... The Install phase completed successfully, and the Commit phase is beginning. See the contents of the log file for the HelloWorldService.exe assembly's progress. The file is located at HelloWorldService.InstallLog. Call Committing. on the HelloWorldService.exe assembly. Affected parameters are: assemblypath = HelloWorldService.exe logfile = HelloWorldService.InstallLog The Commit phase completed successfully. The transacted install has completed.
Using the Services snap-in or the Server Explorer allows you to see the new service. You can see what it will look like in the Services snap-in with Figure 7.
Figure 7 The newly created HelloWorldService.
You can right-click on this service to bring up properties of the service. You can also start or stop the service from this snap-in. In addition, you can start or stop the service from the Server Explorer by right-clicking on the service. You can also view properties and see the current running status of the service. A square red block in the lower-left corner of the icon next to the service name indicates that the service is stopped. A green triangle indicates that the service is started. The services portion of the Server Explorer looks like Figure 8.
Figure 8 The newly created HelloWorldService as viewed by the Server Explorer.
After you start the service, the timer is configured to fire every 100 milliseconds. At that interval, you simply increment the custom performance counter. To accomplish this, you set the appropriate properties on the timer, as well as the performance counter components that you added to the service design window. You also add the code in Listing 21 to perform the modification of the counter.
Listing 21Modifying the Performance Counter
private void OnTimer(object sender, System.Timers.ElapsedEventArgs e) { performanceCounter.Increment(); }
Monitoring the Custom Performance Counter
Now that you have added the new performance counter and built the service, you can monitor the performance counter with the Performance Monitor or programmatically. If you choose to use the Performance Monitor, you will need to select the counter that you want to monitor, as shown in Figure 9.
Figure 9 Selecting the HelloWorld performance counter.
After you have selected the counter, the graph will display the value of the counter at regular intervals, producing a graph that looks like Figure 10.
Figure 10 Monitoring the HelloWorld performance counter.
As you increment the counter, you should see a steadily increasing graph, as shown in Figure 10.
You can also monitor the performance counter programmatically. Using the same tool that was associated with Listings 1719, you can monitor the current value of the custom performance counter. After you have selected the performance counter category and performance counter, you should see something like Figure 11.
Figure 11 Programmatically monitoring the HelloWorld performance counter.
Remember to have the timer set to read the performance counter every second with this application. As you can see from Figure 16, the value increases by 10 with each new display. This indicates that all is working correctly because the performance counter was incremented to 10 times per second (a 100-millisecond interval).
Adding a Counter Programmatically
Instead of using the Server Explorer to create a custom performance counter, you can programmatically create the counter as well. You might want to do this for a custom application that needs to create and destroy performance counters or an application that has a specialized need to have the counter exist only for the life of the application. Many different scenarios can be devised in which it is just easier to create the counter programmatically. A simple application has been devised that creates the same counter that was made using the Server Explorer. The critical portion of this sample is shown in Listing 22.
Listing 22Programmatically Creating a Custom Performance Counter
CounterCreationDataCollection CounterDatas = new CounterCreationDataCollection(); // Create the counters and set their properties. CounterCreationData cdCounter = new CounterCreationData(); cdCounter.CounterName = "Count"; cdCounter.CounterHelp = "Simple counter"; cdCounter.CounterType = PerformanceCounterType.NumberOfItems32; // Add counter to the collection. CounterDatas.Add(cdCounter); // Create the category and pass the collection to it. PerformanceCounterCategory.Create("Hello World", "Hello World performance category", CounterDatas);