Introduction
What impresses potential clients touring your office more than anything? A big screen displaying realtime performance data for your critical servers. I'm joking, but only partially: it makes a company look professional and it's also a huge boon for network operations employees who can tell within seconds if something is wrong with a server. An ideal presentation medium for this is obviously a web page, since you can implement multiple versions of this performance data display for network operations or even for people wishing to check on the status of a server over their phone or PDA and also drive automatic updating of that data. There are several rudimentary web controls and tutorials out there on this subject (such as this article at 4GuysFromRolla), but nothing that was very feature rich or that automatically updated, which I considered to be the most compelling feature of performance counter data display. This kind of surprised me, so I decided to roll my own and develop the ASP.NET web control that you see here today.
Background
First, an overview is necessary regarding performance metrics data in a Windows environment. Windows machines provide an ideal mechanism for capturing this data in the form of performance counters: they monitor system components such as processors, memory, disk I/O, important service instances, etc. and publish various pieces of performance related data. A particular performance counter is defined by four pieces of data, the first of which is the category name of the counter. This, true to its name, is the general category for the performance counter which can be Memory, Processor, Network Interface, etc. The second data point is the counter name which is that performance component in this category for which this monitor is reading data: for instance, in the Memory category, you can monitor such pieces of data as Available MBytes, Pages/sec, System Code Total Bytes, and many others. Third we have the instance name, which is the instance of the counter that we are monitoring. Many performance counters are not instanced, but some gather performance metrics for many members of a particular component. For instance, under the category Process and counter % Process Time, we have an instance for the total percentage of CPU time in use as well as for each process currently running on the system, meaning that we can gather CPU usage data as a whole, or for individual processes as well. The final piece of data for a performance counter is the machine name, which indicates which machine on the network houses the performance counter that we are examining. This means that you can watch performance counters for many machines from a single, central machine which is then responsible for publishing those metrics in the form of a web page. If you want a list of performance counters available on your machine (there is really a stunning amount of data that you can collect), then just go to Start->Run and then type in perfmon.exe. You can then right-click on the graph and go to Add Counters to get a full list of available counters. .NET provides excellent programmatic access to performance counters through the PerformanceCounter
class in the System.Diagnostics
namespace which we will be making heavy use of in this project (you can find MSDN's coverage of this class here).
Implementation
First, a prerequisite for this project: you will need to install Microsoft's ASP.NET AJAX framework, which you can find here. This is necessary not for its AJAX callback functionality, which ASP.NET 2.0 already provides in an extremely basic form (enough for this project), but instead for its enhancements to client-side JavaScript code which I use in most of my projects now. They give client-side code a much more organized, C# feel and also provide enhanced functionality such as inheritence, namespaces, and enumerations. While an exhaustive overview of this functionality is not necessary for this project, interested readers can find out more here.
Basic functionality
For this project, 90% of it was surprisingly easy to implement, but that last 10% contained some interesting problems that required some inventive use of the ASP.NET rendering progression, which will be covered later. First, we'll go over the functionality provided by this control. At a very basic level, it renders performance counter data to the screen and automatically updates itself via an AJAX call after a pre-determined interval. The form that the performance counter data can take on the screen is laid out in the PerformanceCounterDisplayType
enumeration:
public enum PerformanceCounterDisplayType
{
Text,
ProgressBar,
LineGraph,
Histogram
}
As you can see, we can present the data as a simple text label, a progress bar, a historical line graph, or a historical histogram (bar graph). The properties of the control are many of those you would expect to find for a web control along with those that define the performance counter that it is rendering. The CategoryName
, CounterName
, InstanceName
, and MachineName
accomplish the latter, each defining a data point for the performance counter that were discussed in the background section. The RefreshInterval
property defines the amount of time, in seconds, between data refresh attempts for this control. The Floor
and Ceiling
properties are used in rendering progress bars, histograms, and line graphs so that we can determine the range that a performance counter's value might fall in. The HistoryCounter
property is used by histograms and line graphs to determine how many historical data points to display on the screen. The Modifier
property is a value by which the actual value of a performance counter is multiplied prior to returning it, which is useful for converting between different measurement units, like bytes and kilobytes. The Invert
property indicates whether or not we should invert the performance counter's value (subtract it from the Ceiling property) prior to returning it. The Width
, Height
, and CssClass
properties control the appearance of the control's data on the screen. The FormatString
property is used for text displays to format the performance counter's value prior to displaying it on the screen. The Orientation
property is used for progress bars to determine in which direction (horizontally or vertically) the progress bar "grows". Finally, the OnChange
property represents the client-side JavaScript function that should be invoked when the performance counter's data is refreshed. This function should accept a client-side PerformanceCounter
object (which will be covered later) as a parameter and can be used to display warning messages to the user, change the CSS class of the control, etc.
Rendering and value calculation
Now we get down to the interesting implementation details and first up is the actual building of the control hierarchy to display the performance counter data on the screen. So, we must look at the CreateChildControls()
method:
protected override void CreateChildControls()
{
string hashKey = categoryName + ":" + counterName + ":" + instanceName +
":" + machineName;
switch (displayType)
{
case PerformanceCounterDisplayType.Text:
Label textLabel = new Label();
textLabel.CssClass = cssClass;
Controls.Add(textLabel);
break;
case PerformanceCounterDisplayType.ProgressBar:
Label containerLabel = new Label();
Label progressBarLabel = new Label();
containerLabel.Width = width;
containerLabel.Height = height;
progressBarLabel.CssClass = cssClass;
progressBarLabel.Style["position"] = "absolute";
progressBarLabel.Style["overflow"] = "hidden";
if (orientation == RepeatDirection.Vertical)
{
progressBarLabel.Style["bottom"] = "0px";
progressBarLabel.Width = width;
progressBarLabel.Height = 1;
}
else
{
progressBarLabel.Style["left"] = "0px";
progressBarLabel.Height = height;
progressBarLabel.Width = 1;
}
containerLabel.Controls.Add(progressBarLabel);
Controls.Add(containerLabel);
break;
case PerformanceCounterDisplayType.Histogram:
int sampleWidth = Convert.ToInt32(Math.Floor(
(double)(width / historyCount)));
Panel containerPanel = new Panel();
Panel subContainerPanel = new Panel();
containerPanel.Width = width;
containerPanel.Height = height;
containerPanel.Style["position"] = "relative";
subContainerPanel.Width = width;
subContainerPanel.Style["position"] = "absolute";
subContainerPanel.Style["bottom"] = "0px";
Label[] histogramEntries = new Label[historyCount];
for (int i = 0; i < historyCount; i++)
{
histogramEntries[i] = new Label();
histogramEntries[i].CssClass = cssClass;
histogramEntries[i].Width = sampleWidth;
histogramEntries[i].Height = 1;
histogramEntries[i].Style["position"] = "absolute";
histogramEntries[i].Style["left"] = Convert.ToString(i *
sampleWidth)
+ "px";
histogramEntries[i].Style["bottom"] = "0px";
histogramEntries[i].Style["overflow"] = "hidden";
subContainerPanel.Controls.Add(histogramEntries[i]);
}
containerPanel.Controls.Add(subContainerPanel);
Controls.Add(containerPanel);
break;
case PerformanceCounterDisplayType.LineGraph:
Panel lineContainerPanel = new Panel();
lineContainerPanel.Width = width;
lineContainerPanel.Height = height;
lineContainerPanel.Style["position"] = "relative";
Controls.Add(lineContainerPanel);
break;
}
if (performanceCounter == null &&
!performanceCounters.ContainsKey(hashKey))
{
performanceCounter = new PerformanceCounter(categoryName, counterName,
instanceName, machineName);
performanceCounters[hashKey] = performanceCounter;
if (!IsPerformanceCounterInstantaneous(performanceCounter))
{
performanceCounterSamples[hashKey] = new List<CounterSample>();
performanceCounterSamples[hashKey].Add(
performanceCounter.NextSample());
}
}
else if (performanceCounter == null &&
performanceCounters.ContainsKey(hashKey))
performanceCounter = performanceCounters[hashKey];
Page.PreRenderComplete += new EventHandler(Page_PreRenderComplete);
base.CreateChildControls();
}
The creation of the control hierarchy is relatively simple: for text displays we simply create a Label
object, for a progress bar we create a Label
object representing the maximum possible size of the progress bar and another one that represents the current value, for histograms we create a container Panel
object and then Label
objects representing each bar in the graph, and for line graphs we create a container Panel
object that we will later draw to. The interesting code is in the last few lines of the function. Here we create the actual PerformanceCounter
object if it does not already exist. If it does already exist, we use the one stored in the cache; this is to prevent duplicate objects being created in the case where we have several controls that are watching the same performance counter, but are displaying the data in different ways. We also capture the initial counter sample for those counters that require it. This is because some counters require multiple samples in order to calculate their value. A good example of this is network traffic: we need to capture two samples since we need the amount of time that elapsed between the two samples and also the change in the total number of bytes transmitted by the NIC. Once we have both of those values, we can calculate the number of bytes transmitted/second over that time period. However, we need to do this in a two stage process and ensure that a minimum amount of time has elapsed between samples: if we capture two samples back-to-back with only an extremely small amount of time having elapsed between the two, we can't calculate a statistically relevant value. So we do this in two stages in the ASP.NET rendering process to ensure that we can calculate accurate results but also spend the minimum amount of time necessary to do so. The comments have this process outlined, but I'll go over it again here:
- Each
PerformanceCounterControl
in the page will call the CreateChildControls()
method. - For non-instantaneous counters, we will add the initial sample for the counter to the historical data.
PreRenderComplete
event occurs for the page, at which point each control will have their Page_PreRenderComplete()
handler method invoked. - The first non-instantaneous control will call the
Value
property to get the current value of the counter. - Inside the
Value
property, we check to see if at least 100 milliseconds have elapsed since we added the first historical entry in step 2. - If not, then we sleep the thread for 100 milliseconds and then take a counter sample and calculate the value based off of the two historical samples.
- At this point, every subsequent non-instantaneous control will see that more than 100 milliseconds have passed since step 2 and so will not require a thread sleep prior to obtaining a new counter sample and calculating the value.
The aforementioned Page_PreRenderComplete()
method is as follows:
protected void Page_PreRenderComplete(object sender, EventArgs e)
{
if (!Page.ClientScript.IsStartupScriptRegistered(GetType(),
"PerformanceCounterInitialize"))
Page.ClientScript.RegisterStartupScript(GetType(),
"PerformanceCounterInitialize",
"var performanceCounters = new Array();\n", true);
string childIDs = "";
switch (displayType)
{
case PerformanceCounterDisplayType.Text:
case PerformanceCounterDisplayType.LineGraph:
childIDs = "'" + Controls[0].ClientID + "'";
break;
case PerformanceCounterDisplayType.ProgressBar:
if (orientation == RepeatDirection.Horizontal)
((Label)Controls[0].Controls[0]).Width =
Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
(Value - floor) / ceiling * width), width), 1));
else
((Label)Controls[0].Controls[0]).Height =
Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
(Value - floor) / ceiling * height), height), 1));
childIDs = "'" + Controls[0].Controls[0].ClientID + "'";
break;
case PerformanceCounterDisplayType.Histogram:
foreach (Control control in Controls[0].Controls[0].Controls)
childIDs += "'" + control.ClientID + "', ";
((Label)Controls[0].Controls[0].Controls[historyCount - 1]).Height =
Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
(Value - floor) / ceiling * height), height), 1));
childIDs = childIDs.Substring(0, childIDs.Length - 2);
break;
}
Page.ClientScript.GetCallbackEventReference(this, null,
"RenderPerformanceCounter", "'" + ClientID + "'");
Page.ClientScript.RegisterClientScriptResource(typeof(ScriptManager),
"MicrosoftAjax.js");
Page.ClientScript.RegisterClientScriptResource(typeof(ScriptManager),
"MicrosoftAjaxWebForms.js");
Page.ClientScript.RegisterClientScriptResource(GetType(),
"Stratman.Web.UI.Resources.PerformanceCounter.js");
Page.ClientScript.RegisterStartupScript(GetType(), ClientID,
String.Format("performanceCounters['{0}'] = new " +
"Stratman.Web.UI.PerformanceCounter('{0}', " +
"Stratman.Web.UI.PerformanceCounterDisplayType.{1}, " +
"{2}, {3}, {4}, {5}, {6}, {7}, '{8}', " +
"Sys.UI.RepeatDirection.{9}, {10}, '{11}', '{12}', " +
"'{13}', '{14}', '{15}', {16}, {17});\n", ClientID,
displayType.ToString(), Value, refreshInterval, width,
height, ceiling, floor, formatString, orientation,
historyCount, cssClass, categoryName, counterName,
instanceName, machineName,
(onChange == "" ? "null" : onChange), childIDs), true);
if (displayType == PerformanceCounterDisplayType.LineGraph)
Page.ClientScript.RegisterClientScriptResource(GetType(),
"Stratman.Web.UI.Resources.VectorGraphics.js");
}
As was mentioned previously, it sets the value of the control by setting the text contents for text displays or by sizing child elements for progress bars or histograms. It also registers the necessary client script includes and emits the JavaScript snippet that instantiates the client-side instance of the control. Finally, the Value
property is as follows:
public float Value
{
get
{
EnsureChildControls();
string hashKey = categoryName + ":" + counterName + ":" + instanceName +
":" + machineName;
if (IsPerformanceCounterInstantaneous(performanceCounter))
{
float calculatedValue = (invert ?
ceiling - performanceCounter.NextValue() :
performanceCounter.NextValue());
return (modifier != 0 ? calculatedValue * modifier : calculatedValue);
}
else
{
List<CounterSample> samples = performanceCounterSamples[hashKey];
CounterSample previousSample =
samples[performanceCounterSamples[hashKey].Count - 1];
if (performanceCounterSamples[hashKey].Count == 1 &&
DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec <
1000000)
Thread.Sleep(100);
if (DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec >=
1000000)
{
if (performanceCounterSamples[hashKey].Count > 1)
performanceCounterSamples[hashKey].RemoveAt(0);
samples.Add(performanceCounter.NextSample());
}
float calculatedValue = CounterSample.Calculate(samples[0],
samples[1]);
calculatedValue = (invert ? ceiling - calculatedValue :
calculatedValue);
return (modifier != 0 ? calculatedValue * modifier : calculatedValue);
}
}
}
So we see that we simply call the GetNextValue()
method of PerformanceCounter
if this is an instantaneous counter, otherwise we have to calculate between two counter samples. If this is the case and we have only one historical sample (i.e. this is the first time this counter was instantiated and we have not rendered anything to the screen yet), then we check to see how much time has passed since we collected that first sample: if it's less than 100 milliseconds, then we sleep the thread for that long. Afterwards, we collect another sample, calculate the value using the two samples, modify/invert it as necessary, and return it. If, however, we had more than one historical sample then we check the timestamp on the last sample. If it was collected more than 100 milliseconds ago, we collect another sample and calculate the value based on the previous sample and the sample we just collected. Otherwise, we just use the previous two samples to calculate the value (without collecting a new one).
Client-side scripting and callbacks
The JavaScript resource file that drives scripting and client-side updates of the control is Resources\PerformanceCounter.js
. The client-side class for the control is Stratman.Web.UI.PerformanceCounter
(note the namespacing, which is possible through the ASP.NET AJAX extensions). It has all of the usual methods you would expect, including Render()
which renders the counter data on the screen and SetCssClass()
which updates the CssClass
being used for the control. The code for the Render()
method is as follows:
Stratman.Web.UI.PerformanceCounter.prototype.Render = function()
{
if (this.Type == Stratman.Web.UI.PerformanceCounterDisplayType.Text)
document.getElementById(this.ChildElementIDs[0]).innerHTML =
String.format(this.FormatString, this.Value);
else if (this.Type ==
Stratman.Web.UI.PerformanceCounterDisplayType.ProgressBar)
{
if (this.Orientation == Sys.UI.RepeatDirection.Vertical)
document.getElementById(this.ChildElementIDs[0]).style.height =
Math.round(Math.max(Math.min(
(this.Value - this.Floor) / this.Ceiling, 1) * this.Height, 1)) +
"px";
else
document.getElementById(this.ChildElementIDs[0]).style.width =
Math.round(Math.max(Math.min(
(this.Value - this.Floor) / this.Ceiling, 1) * this.Width, 1)) +
"px";
}
else if (this.Type ==
Stratman.Web.UI.PerformanceCounterDisplayType.Histogram)
{
for (var i = 0; i < this.HistoryCount; i++)
document.getElementById(this.ChildElementIDs[i]).style.height =
Math.max(Math.min(
(this.HistorySamples[i] - this.Floor) / this.Ceiling, 1) *
this.Height, 1) +
"px";
}
else if (this.Type ==
Stratman.Web.UI.PerformanceCounterDisplayType.LineGraph)
{
var sampleWidth = Math.round(this.Width / this.HistoryCount);
var lineHTML = "";
this.VectorGraphics.setCssClass(this.CssClass);
this.VectorGraphics.clear();
for (var i = 0; i < this.HistoryCount - 1; i++)
this.VectorGraphics.drawLine((i * sampleWidth),
this.Height - Math.round(Math.min((this.HistorySamples[i] -
this.Floor) / this.Ceiling, 1) * (this.Height - 1)) - 1,
((i + 1) * sampleWidth),
this.Height - Math.round(Math.min((this.HistorySamples[i + 1] -
this.Floor) / this.Ceiling, 1) * (this.Height - 1)) - 1);
this.VectorGraphics.paint();
}
}
The rendering of the control functions exactly like it does in the actual C# code with the exception of the line graphs. Here, we make use Walter Zorn's excellent JavaScript vector graphics library to take care of drawing the line graph. In the constructor for the client-side PerformanceCounter
class, we created a jsGraphics
object, passing in the DOM object representing the container panel we had created in the control hierarchy. To draw lines, we simply call drawLine()
for each entry in the history data to link them together and then call paint()
to actually render the lines to the container control.
For updating the performance counter data, we make use of batched AJAX calls to the server. To accomplish this, at the conclusion of the constructor for the client-side PerformanceCounter
object, a call to the RegisterForRefresh
method is made. This method is as follows:
function RegisterForRefresh(id, refreshInterval)
{
if (refreshHash[refreshInterval] == null)
{
refreshHash[refreshInterval] = [];
window.setTimeout("UpdatePerformanceCounters(" + refreshInterval +
")", refreshInterval * 1000);
}
Array.add(refreshHash[refreshInterval], id);
}
All this function does is create an array entry for this refresh interval if it does not already exist and calls setTimeout()
to kick off the callback process after the specified interval. It then adds the client ID for this performance counter control to the array for the refresh interval. Why do we do it this way? It's to avoid a ton of unnecessary calls to the server: we batch up the IDs of all of the controls that are supposed to be refreshed at the given interval and make a single call back to the server to get all of their values. This way, we avoid unnecessary calls from the client and unnecessary load on the server. Anyway, the code for the UpdatePerformanceCounters()
method is as follows:
function UpdatePerformanceCounters(refreshInterval)
{
var performanceCounterIDs = refreshHash[refreshInterval][0];
for (var i = 1; i < refreshHash[refreshInterval].length; i++)
performanceCounterIDs += "," + refreshHash[refreshInterval][i];
WebForm_DoCallback(refreshHash[refreshInterval][0], performanceCounterIDs,
RenderPerformanceCounters, refreshInterval, null,
false);
window.setTimeout("UpdatePerformanceCounters(" + refreshInterval + ")",
refreshInterval * 1000);
}
All it does is batch up the control IDs for this interval, do a callback for a control via WebForm_DoCallback()
, and re-registers itself via another setTimeout()
call. At this point we're back on the server: in order for a control class to be eligible for callbacks via WebForm_DoCallback()
, it has to implement the ICallbackEventHandler
interface, which we've done. It requires two methods to be implemented, the first of which is RaiseCallbackEvent()
which is responsible for doing any parsing of the argument (the string of control IDs in our case) necessary. This method is as follows:
public void RaiseCallbackEvent(string eventArgument)
{
performanceCounterIDs = eventArgument.Split(',');
}
So, it just splits the control ID string into individual IDs. The next method is GetCallbackResult()
which is responsible for actually handling the callback:
public string GetCallbackResult()
{
StringBuilder performanceCounterValues = new StringBuilder();
foreach (string performanceCounterID in performanceCounterIDs)
{
PerformanceCounterControl performanceCounterControl =
(PerformanceCounterControl)FindControlByClientID(
performanceCounterID, Page);
performanceCounterValues.Append(performanceCounterControl.Value + ",");
}
string returnValue = performanceCounterValues.ToString();
return returnValue.Substring(0, returnValue.Length - 1);
}
This one is also pretty simple: it just searches through the control hierarchy of the page for each control that we're supposed to update, gets its latest value, and concatenates it to a growing string. It then returns that list of values to the client at which point the success event handler of our AJAX callback, RenderPerformanceCounters()
, is invoked:
function RenderPerformanceCounters(response, context)
{
var performanceCounterValues = response.split(",");
for (var i = 0; i < performanceCounterValues.length; i++)
{
var performanceCounter = performanceCounters[refreshHash[context][i]];
performanceCounter.Value =
Number.parseInvariant(performanceCounterValues[i]);
if (performanceCounter.Type ==
Stratman.Web.UI.PerformanceCounterDisplayType.LineGraph ||
performanceCounter.Type ==
Stratman.Web.UI.PerformanceCounterDisplayType.Histogram)
{
var samples = performanceCounter.HistorySamples;
for (var x = 0; x < performanceCounter.HistoryCount - 1; x++)
performanceCounter.HistorySamples[x] = samples[x + 1];
samples[performanceCounter.HistoryCount - 1] =
performanceCounter.Value;
}
performanceCounter.Render();
if (performanceCounter.OnChange)
performanceCounter.OnChange(performanceCounter);
}
}
We've now entered the last leg of the update process. We split the value string into the individual values and then update the control's values, including the historical data for histograms and line graphs. We then call the Render()
method for each control and, if it's set, the OnChange
event handler function.
Usage
For demonstration purposes I've included a test website called PerformanceCountersTest with the source download, a screenshot of which can be seen at the top of this article. There's not really much to go over with it: it's a re-implementation of a lot of the Windows Task Manager performance monitor functionality and it basically consists of some layout CSS and a bunch of PerformanceCounterControl
instances. It's a great indicator of just how easy it is to develop a rich performance monitoring web page with this control.
History
2007-03-04 - Initial publication.