Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

AJAX-enabled Performance Counter Web Control

4.66/5 (14 votes)
4 Mar 2007BSD13 min read 2   1.2K  
ASP.NET web control that renders performance counter data to the screen in the form of a text label, progress bar, histogram, or line graph and automatically updates itself via an AJAX call
Screenshot - performancecountercontrol.gif

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:

C#
public enum PerformanceCounterDisplayType
{
  /// <summary>
  /// Display the current value of the counter.
  /// </summary>
  Text,
  /// <summary>
  /// Display a single, updating bar representing the percentage value of the 
  /// counter between its floor and ceiling.
  /// </summary>
  ProgressBar,
  /// <summary>
  /// Display a line graph of the counter's historical data.
  /// </summary>
  LineGraph,
  /// <summary>
  /// Display a histogram (bar graph) of the counter's historical data.
  /// </summary>
  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:

C#
protected override void CreateChildControls()
{
  string hashKey = categoryName + ":" + counterName + ":" + instanceName + 
                   ":" + machineName;

  switch (displayType)
  {
    // In the case of a text display type, simply create a Label child object
    case PerformanceCounterDisplayType.Text:
      Label textLabel = new Label();
      textLabel.CssClass = cssClass;

      Controls.Add(textLabel);
      break;

    // In the case of a progress bar, create a label whose width and height 
    // represent the maximum width and height of the progress bar and 
    // another label that will represent the current value of the counter
    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";

      // Set the actual progress bar's style attributes according to 
      // whether it grows horizontally or vertically
      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;

    // In the case of a histogram, create a container panel and a 
    // sub-container panel, the latter of which will hold the histogram 
    // bars
    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";

      // Two panels are necessary so that the histogram bars are rendered 
      // properly
      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;

    // In the case of a line graph, simply create a container panel:  the 
    // vector graphics JavaScript library will take care of actually 
    // drawing the graph
    case PerformanceCounterDisplayType.LineGraph:
      Panel lineContainerPanel = new Panel();

      lineContainerPanel.Width = width;
      lineContainerPanel.Height = height;
      lineContainerPanel.Style["position"] = "relative";

      Controls.Add(lineContainerPanel);
      break;
  }

  // Create the performance counter object if it doesn't already exist and, 
  // if it's a non-instantaneous counter, add a sample to the history for it; 
  // we add a sample now so that when we go to actually calculate the initial 
  // value in the PreRenderComplete event handler, sufficient time will have 
  // passed:
  //  1. All controls call CreateChildControls(), adding the initial counter 
  //     sample to the history
  //  2. PreRenderComplete event handler is called for the first control:  it 
  //     gets the counter value from the Value property
  //  3. If the Value property detects that less than 100 milliseconds have 
  //     passed since the history entry was added in CreateChildControls, it 
  //     sleeps for 100 milliseconds and then gets another sample
  //  4. At that point, the calls to the Value property for all other 
  //     controls will occur after the necessary 100 milliseconds and no more 
  //     pausing will be necessary
  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());
    }
  }

  // If the counter object already exists, just use the copy from the cache
  else if (performanceCounter == null && 
           performanceCounters.ContainsKey(hashKey))
    performanceCounter = performanceCounters[hashKey];

  // Add an event handler to the page's pre-render complete event
  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:

  1. Each PerformanceCounterControl in the page will call the CreateChildControls() method.
  2. For non-instantaneous counters, we will add the initial sample for the counter to the historical data.
  3. PreRenderComplete event occurs for the page, at which point each control will have their Page_PreRenderComplete() handler method invoked.
  4. The first non-instantaneous control will call the Value property to get the current value of the counter.
  5. 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.
  6. 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.
  7. 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:

C#
protected void Page_PreRenderComplete(object sender, EventArgs e)
{
  // Add the client-side performance counter initialization snippet
  if (!Page.ClientScript.IsStartupScriptRegistered(GetType(), 
         "PerformanceCounterInitialize"))
    Page.ClientScript.RegisterStartupScript(GetType(), 
       "PerformanceCounterInitialize", 
       "var performanceCounters = new Array();\n", true);

  string childIDs = "";

  // Get the list of client IDs for all child elements for client-side 
  // registration
  switch (displayType)
  {
    case PerformanceCounterDisplayType.Text:
    case PerformanceCounterDisplayType.LineGraph:
      childIDs = "'" + Controls[0].ClientID + "'";
      break;

    // For progress bars and histograms, get the value of performance 
    // counters and render the data appropriately
    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;
  }

  // Register the necessary client script resources and emit the registration 
  // snippet for this control
  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);

  // Only include the vector graphics library if the control is a line graph
  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:

C#
public float Value
{
  get
  {
    // Make sure that child controls have been created first
    EnsureChildControls();

    string hashKey = categoryName + ":" + counterName + ":" + instanceName + 
                     ":" + machineName;

    // If the performance counter is instantaneous (i.e. requires no 
    // historical data) then just get the current value for it, modify/invert 
    // it if necessary, and return it
    if (IsPerformanceCounterInstantaneous(performanceCounter))
    {
      float calculatedValue = (invert ? 
         ceiling - performanceCounter.NextValue() : 
         performanceCounter.NextValue());
    
      return (modifier != 0 ? calculatedValue * modifier : calculatedValue);
    }

    else
    {
      List<CounterSample> samples = performanceCounterSamples[hashKey];

      // Get the previous sample for this counter
      CounterSample previousSample = 
         samples[performanceCounterSamples[hashKey].Count - 1];

      // If less than 100 milliseconds have passed since the previous sample 
      // was obtained and we have only one historical sample then sleep the 
      // thread; this is to ensure that enough time has passed between 
      // samples for a statistically relevant value to be calculated
      if (performanceCounterSamples[hashKey].Count == 1 && 
          DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec < 
          1000000)
        Thread.Sleep(100);

      // If more than 100 milliseconds have passed, then obtain a new sample 
      // and record it in this history data; otherwise we just use the 
      // previous two samples to calculate the value
      if (DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec >= 
          1000000)
      {
        if (performanceCounterSamples[hashKey].Count > 1)
          performanceCounterSamples[hashKey].RemoveAt(0);

        samples.Add(performanceCounter.NextSample());
      }

      // Calculate the value, modify/invert it if necessary, and then return 
      // it
      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:

JavaScript
Stratman.Web.UI.PerformanceCounter.prototype.Render = function()
{
  // For text displays, simply call String.format()
  if (this.Type == Stratman.Web.UI.PerformanceCounterDisplayType.Text)
    document.getElementById(this.ChildElementIDs[0]).innerHTML = 
       String.format(this.FormatString, this.Value);
    
  // For progress bars, just set the width or height (depending on the 
  // orientation) of the progress bar
  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";
  }
        
  // For histograms, set the height of each bar to the value of the 
  // corresponding entry in the history data
  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";
  }
    
  // For line graphs, call the drawLine() function in the vector graphics 
  // library for each entry in the history data
  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:

JavaScript
function RegisterForRefresh(id, refreshInterval)
{
  // Create the refresh hash entry for this interval if it doesn't already 
  // exist and make a call to setTimeout() to initialize the callback
  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:

JavaScript
function UpdatePerformanceCounters(refreshInterval)
{
  // Assemble the list of control IDs to update into a comma-delimited 
  // string
  var performanceCounterIDs = refreshHash[refreshInterval][0];

  for (var i = 1; i < refreshHash[refreshInterval].length; i++)
    performanceCounterIDs += "," + refreshHash[refreshInterval][i];

  // Make the callback to the web server and call setTimeout() again to 
  // re-register this callback
  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:

C#
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:

C#
public string GetCallbackResult()
{
  StringBuilder performanceCounterValues = new StringBuilder();

  // Find each control and get its updated value
  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:

JavaScript
function RenderPerformanceCounters(response, context)
{
  var performanceCounterValues = response.split(",");
    
  // Loop through each control that we're to update
  for (var i = 0; i < performanceCounterValues.length; i++)
  {
    var performanceCounter = performanceCounters[refreshHash[context][i]];
    performanceCounter.Value = 
       Number.parseInvariant(performanceCounterValues[i]);
        
    // Update the history data for line graphs and histograms
    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;
    }
        
    // Render the control
    performanceCounter.Render();
        
    // Invoke the value changed event handler, if it's set
    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.

License

This article, along with any associated source code and files, is licensed under The BSD License