Introduction
One of the features of Microsoft Windows is performance counters. A performance counter is just an integer managed by Windows; it can be added to, subtracted from, and have its value set. Windows comes with many of these to count things like disk I/Os performed. You can read these, but not change them.
You can also create custom performance counters, which can be both read and changed. Custom performance counters can be used to instrument applications, e. g., number of order received, value of goods sold, number of errors encountered. For the rest of the post I'll refer to custom performance counters as just counters.
Using counters requires software for the following:
- defining the counters to Windows
- setting the value of the counters
- reading the counters
- calculating a metric from the counters
- creating useful information from the metric data
At the end of this post is a sample program to demonstrate topics 1 through 4.
Defining the Counters to Windows
All performance counters, custom or those defined by Windows, are a member of one and only one performance counter category. To define counters, you pass to Windows a performance counter category definition, which contains the definition of all the counters in the category. Thus, the category and all its counters are created in one shot. Furthermore, once a category is created, it can't be changed, although you can delete the category and create a new category with the same name and a different definition.
There are two approaches to creating a performance counter category. If the counters are used by an application that has an installer, then creating the category could be part of the install. The other approach is that any piece of software that wants to write to the counters checks during it initialization for the existance of the category; if the category is missing then it's created. See the function establishCounters in the sample program for details.
Sometimes counters are related. This occurs when you want to calculate an average. One counter is the numerator and another the denominator. Microsoft didn't take a very object-oriented approach to this. Instead of one counter referencing the other, the relationship is implied through the counter type and the order it is added to the category. Again, see establishCounters for details.
Setting the Value of the Counters
The instrumented application is responsible for setting the counter values. If you want metrics about orders received, then the instrumented application would increment an order counter. If you wanted to know the average sale price per order, then both a sale price and order counter would be incremented.
The System.Diagnostics.PerformanceCounter class has three functions for changing a counter's value: Increment (add 1), Decrement (subtract 1), and IncrementBy (add a long, either a positive or negative number). Increment and Decrement are much faster than IncrementBy. You can also set a counter to any value by assigning to the RawValue property. See the function writeSample in the sample program for details.
Reading the Counters
In and of itself, setting the value of the counters doesn't do anything useful. Something has to read the counter values to create metrics. Most metrics are time series based. Knowing the number of orders received since the server was last booted isn't meaningful. A time series of number of orders received per hour is meaningful.
Windows doesn't know anything about time series. It simply knows the current value of the counters. Thus, to calculate a time series, software is needed to read counters periodically to capture the baseline (before) and current value. A time series value is calculated using both the baseline and current value. This is typically done by a separate analysis application. The Performance monitor that comes with Windows is an example of such an application. You can also write you own analysis application.
Note that your custom performance counters are available to the Performance monitor and your analysis application can access Windows performance counters. This allows you to examine relationships between your application and Windows by examining, for example, orders received and CPU utilization together. See the function dumpMetrics in the sample program for details.
Calculating a Metric from the Counters
You could calculate a metric simply by "doing the math", but the .NET framework comes with a class, System.Diagnostics.CounterSampleCalculator, that gives you standard calculations based on the type of counter. The types of counters and their calculation can be found on the System.Diagnostics.PerformanceCounterType Enumeration .NET framework MSDN reference page. See the function dumpMetrics in the sample program for details.
Creating Useful Information from the Metric Data
Once you've calculated a metric you need to turn it into useful information. You could display it in real time (like the Windows performance monitor), or, probably more practical, you could store it in a relational database for later analysis. You could then run queries or connect a front end such as Excel or SQL Server Reporting Services.
The Sample Program
using System;
using System.Diagnostics;
using System.Threading;
using System.Collections.Generic;
using System.Text;
using System.Timers;
using System.Runtime.InteropServices;
namespace PerformanceCounterDemo {
///
/// program to demonstrate performance counter usage
///
class Program {
///
/// the number of counters being worked with
///
private const int numberOfCounters = 3;
///
/// counters contains the performance counters used in the demonstration
///
static private PerformanceCounter[] counters = new PerformanceCounter[numberOfCounters];
///
/// baseline holds the "old" values used in the calculation
///
static private CounterSample[] baseline = new CounterSample[numberOfCounters];
///
/// index into counters and baseline for AverageTime data
///
private const int averageTimeSlot = 0;
///
/// index into counters and baseline for AverageTimeBase data
///
private const int averageTimeBaseSlot = 1;
///
/// index into counters and baseline for NumberOfOperations data
///
private const int operationsSlot = 2;
///
/// name for the group of counters
///
private const string categoryName = "PerformanceCounterDemo";
///
/// name of performance counter used to calculate average time
///
private const string AverageTimeName = "AverageTime";
///
/// demoninator used to calculate the average time
///
private const string averageTimeBaseName = "AverageTimeBase";
///
/// name of performance counter that counts number of operations completed
///
private const string operationsName = "NumberOfOperations";
///
/// Win32 function to get current time (in ticks)
///
/// returned time
[DllImport("Kernel32.dll")]
public static extern void QueryPerformanceCounter(ref long ticks);
static void Main(string[] args) {
long startTime = 0;
long endTime = 0;
establishCounters();
initialize();
// first period - simulate a period of time where something happens
Console.WriteLine("------------------------------------------------------");
// first sample - tell windows that something took 1 second and completed 4 operations
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(1000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 4);
// second sample - tell windows that something took 2 second and completed 3 operations
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(2000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 3);
// third sample - tell windows that something took 8 second and completed 10 operations
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(8000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 10);
// end of first reporting period - calculate and print results
dumpMetrics();
// second period - similar to first, diffent numbers
Console.WriteLine("------------------------------------------------------");
// first sample
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(3000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 10);
// second sample
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(4000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 9);
// third sample
QueryPerformanceCounter(ref startTime);
System.Threading.Thread.Sleep(5000);
QueryPerformanceCounter(ref endTime);
writeSample(startTime, endTime, 30);
dumpMetrics();
Console.WriteLine("done");
Console.ReadLine();
}
///
/// dump to the console the results for the period
///
static private void dumpMetrics() {
// get the current counter values
CounterSample[] current = new CounterSample[numberOfCounters];
current[averageTimeSlot] = counters[averageTimeSlot].NextSample();
current[averageTimeBaseSlot] = counters[averageTimeBaseSlot].NextSample();
current[operationsSlot] = counters[operationsSlot].NextSample();
// figure out what happened during the period based on the current and baseline values
float timePerOperation = CounterSampleCalculator.ComputeCounterValue(baseline[averageTimeSlot], current[averageTimeSlot]);
float operations = CounterSampleCalculator.ComputeCounterValue(baseline[operationsSlot], current[operationsSlot]);
// dump the raw counter values to the console
Console.WriteLine("Source\t\tAverageTime\tAverageTimeBase\tNumberOfOperations");
Console.Write("baseline\t");
Console.Write(baseline[averageTimeSlot].RawValue.ToString().PadLeft(11) + "\t");
Console.Write(baseline[averageTimeSlot].BaseValue.ToString() + "\t\t");
Console.WriteLine(baseline[operationsSlot].RawValue.ToString());
Console.Write("current\t\t");
Console.Write(current[averageTimeSlot].RawValue.ToString().PadLeft(11) + "\t");
Console.Write(current[averageTimeSlot].BaseValue.ToString() + "\t\t");
Console.WriteLine(current[operationsSlot].RawValue.ToString());
// dump the calculated metric values to the console
Console.WriteLine("time / operation = " + timePerOperation.ToString());
Console.WriteLine("operations = " + operations.ToString());
// make the current values the new baseline
baseline[averageTimeSlot] = current[averageTimeSlot];
baseline[averageTimeBaseSlot] = current[averageTimeBaseSlot];
baseline[operationsSlot] = current[operationsSlot];
}
///
/// the performance counters must be defined to Windows before they can be used
///
static void establishCounters() {
// if you change the definition of the performance counter category in any way
// then it needs to be unconditionaly deleted and recreated
//if (PerformanceCounterCategory.Exists(categoryName)) PerformanceCounterCategory.Delete(categoryName);
// if the performance category doesn't exist, then create it
if (!PerformanceCounterCategory.Exists(categoryName)) {
// create a collection to hold all the performance counters to be created
CounterCreationDataCollection counterData = new CounterCreationDataCollection();
// create the counters and add them to the collection
CounterCreationData counter = new CounterCreationData(AverageTimeName, "time per operation", PerformanceCounterType.AverageTimer32);
counterData.Add(counter);
// NB! the following counter is the "base" for the counter above.
// There's 2 things that make it the base: a type of AverageBase and
// the order of the add call. A base must always be added to the
// collection right after the counter it is the base for.
counter = new CounterCreationData(averageTimeBaseName, "denominator for average time", PerformanceCounterType.AverageBase);
counterData.Add(counter);
counter = new CounterCreationData(operationsName, "number of operations performed", PerformanceCounterType.CounterDelta32);
counterData.Add(counter);
// tell Windows to create the performance category and all its counters
PerformanceCounterCategory.Create(categoryName, "performance counter demo", PerformanceCounterCategoryType.SingleInstance, counterData);
}
}
///
/// add to the counters
///
/// the start time for what is being measured
/// the end time for what is being measured
/// the number of operations completed
private static void writeSample(long startTime, long endTime, int operations) {
// add to the time consumed counter
counters[averageTimeSlot].IncrementBy(endTime - startTime);
// add to the average denominator
counters[averageTimeBaseSlot].IncrementBy(operations);
// add to the count of operations performed
counters[operationsSlot].IncrementBy(operations);
// diagnostic
Console.WriteLine((endTime - startTime).ToString() + "\t" + operations.ToString());
}
///
/// get counters ready for use
///
private static void initialize() {
// get the counters we want to work with from Windows
// note that false must be passed for the third argument if the counter is to be updated
counters[averageTimeSlot] = new PerformanceCounter(categoryName, AverageTimeName, false);
counters[averageTimeBaseSlot] = new PerformanceCounter(categoryName, averageTimeBaseName, false);
counters[operationsSlot] = new PerformanceCounter(categoryName, operationsName, false);
// set the first baseline to the current counter values
baseline[averageTimeSlot] = counters[averageTimeSlot].NextSample();
baseline[averageTimeBaseSlot] = counters[averageTimeBaseSlot].NextSample();
baseline[operationsSlot] = counters[operationsSlot].NextSample();
}
}
}