Using SignalR to power ASP.NET Dashboards, Part 2

In a previous post, I covered how you can use SignalR to provide real-time communications between a Windows application and a Web page. The idea is that the Windows application could collect different kinds of system health information and provide it to the Web page, which serves as a dashboard. I provided online demos and downloadable source to help get the point across. The dashboard demo was fairly realistic, in that it showed several "servers" with color-coded health indicators. Unfortunately, the Windows application was less so. It consisted of several slider controls that set the "health" values that were communicated to the dashboard.

I have been asked to provide a more-realistic Windows application, and that is the point of this post.

 

My new Windows application uses .NET performance-counter classes to collect health information:

It uses SignalR to send this information to the Web page:

The application samples its counters once per second and sends the information to the dashboard. The values that determine the system health status are derived from the thresholds defined in the PAL tool. (PAL is a great addition to your toolbox, by the way. If you haven't used it, you should go look right away, Go ahead, I'll wait.)

 

Try it out

I will describe the new Windows application below. However, if you would like to try this for yourself first, then follow these steps:

  1. Navigate to the web page. The URL is https://signalrdashboard.azurewebsites.net/ .
  2. Run the new Windows application. It is a ClickOnce-deployed application hosted at https://signalrdashboard.blob.core.windows.net/wpfclientperf/publish.htm . (You will probably need to add https://signalrdashboard.blob.core.windows.net to your trusted sites list in order to run the application. It was signed with an untrusted, temporary certificate.)
  3. Copy the "Client ID" value from the web page and paste it into the Windows application. (Because this demo is hosted in the cloud, many people may try to use it at the same time. To keep things simple, each instance of the Windows application will send data only to the client whose ID matches that entered into the application's "Client ID" textbox.)
  4. Sit back and watch the show. New system status values will be set to the dashboard every second. If you get bored with constant "Normal" values, make your machine work harder. For this purpose, I like to make SQL Server Management Studio run "CPU Stress" scripts such as the one described at https://blog.sqlauthority.com/2013/02/22/sql-server-t-sql-script-to-keep-cpu-busy/ . Choose what you will, but get that CPU util up!
  5. OPTIONAL. You can also run the older Windows application as well. The dashboard will respond to the old "health" sliders as well as to the new performance-based information.

 

Read the details

The heart of the new application is the System.Diagnostics.PerformanceCounter class. The application uses this class to pull system performance-counter information from the local machine. In fact, most of the code is devoted to this. There are three main classes involved:

  • SignalRDashboard.Common.PerformanceCounter
    • Abstract class
    • Provides functionality needed to collect data from any system performance counter
  • SignalRDashboard.Common.CPUPerformanceCounter
    • Derived from SignalRDashboard.Common.PerformanceCounter
    • Contains overrides unique to the "Processor Information\% Processor Time(_Total)" counter
  • SignalRDashboard.Common.ProcessorQueueLengthPerformanceCounter
    • Derived from SignalRDashboard.Common.PerformanceCounter
    • Contains overrides unique to the "Processor Queue Length\Processor Information(Total)" counter

 

Combined together, these classes provide the information needed to determine the "health" of the monitored system. (For the demo, this is always the local machine. However, in reality, it could include data from remote machines as well.)

The data points used are:

  • The last five counter values
  • The average of these five values
  • The peak from these five values
  • The warning threshold (from PAL)
  • The critical threshold (from PAL)

The classes also contain logic to update their counter values when directed, and to determine a new health status from those values. The code is too long to duplicate here, but it is all very straight forward and is included in the download zip below.

 

The SignalRDashboard.Common.PerfStatusCounter class brings everything together. This class holds a collection of PerformanceCounter-derived objects, and performs aggregations across them.

 public ICollection<PerformanceCounter> Counters { get; private set; } 

 

For example, the overall status of the system is the maximum of the status values for all the counters:

 public Status Status 
{ 
    get
    { 
        return this.Counters.Max(x => x.Status); 
    } 
} 

 

Similarly, the details of the current status are the concatenation of the details of each counter:

 public ICollection<string> StatusDetails 
{ 
    get
    { 
        var details = new Collection<String>(); 

        foreach (var c in this.Counters) 
        { 
            foreach (var d in c.StatusDetails) 
            { 
                details.Add(d); 
            } 
        } 

        return details; 
    } 
} 

 

And the individual counters are updated as a batch:

 public void Update() 
{ 
    foreach (var c in this.Counters) 
    { 
        c.Update(); 
    } 

    if (!String.IsNullOrWhiteSpace(this.ClientId)) 
    { 
        SignalRSupport.Instance.SendPerfStatusUpdate(this.ClientId, 
                                                    this.CounterName, 
                                                    this.Status.ToString(), 
                                                    this.StatusDetails); 
    } 

    this.OnPropertyChanged("Status"); 
    this.OnPropertyChanged("StatusDetails"); 
} 

 

Notice the "SendPerfStatusUpdate" method above. This is where SignalR comes in. This method makes a call to the "SendPerfStatusUpdate" method defined in the SignalR hub defined in the Web page.

In Common.SignalRSupport:

 public void SendPerfStatusUpdate(string clientId, 
 string systemName, 
 string status, 
 ICollection<string> statusDetails) 
{ 
    if (!String.IsNullOrWhiteSpace(clientId)) 
    { 
        // invoke the SendStatus method that is defined on the hub.
        statusHubProxy.Invoke("SendPerfStatusUpdate", 
 clientId, 
 systemName, 
 status, 
 statusDetails); 
    } 
} 

 

In StatusHub in the Web project:

 public void SendPerfStatusUpdate(string connectionId, 
 string systemName, 
 string status, 
 ICollection<string> statusDetails) 
{ 
    Clients.Client(connectionId).ProcessPerfStatusMessage(systemName, 
 status, 
 statusDetails); 
} 

 

And in the web page itself:

 hub.client.processPerfStatusMessage = function (systemName, 
 status, 
 statusDetails) { 
    // show the row
    $("#" + systemName + "Row").css("visibility", "visible"); 

    // get appropriate class from status
    var c = "btn btn-lg"; 

    if (status == "Critical") { 
        c = "btn btn-lg btn-danger"; 
    } 

    if (status == "Warning") { 
        c = "btn btn-lg btn-warning"; 
    } 

    if (status == "Normal") { 
        c = "btn btn-lg btn-success"; 
    } 

    // assign the class to the appropriate button
    $("#" + systemName).attr("class", c); 

    // assign details
    var detailsName = "#" + systemName + "Details"; 

    $(detailsName).text(""); 

    $.each(statusDetails, function (index, value) { 
        $(detailsName).append(value); 
        $(detailsName).append("<br />"); 
    }); 

Finally, the Windows application has a dispatcher timer that causes the counter values to be updated once per second. Here is the entire code-behind:

 private Common.PerfStatusCounter _status = null; 
private DispatcherTimer _timer = null; 
 
public MainWindow() 
{ 
    InitializeComponent(); 

    var localMachineName = Environment.MachineName; 

    _status = new Common.PerfStatusCounter(); 
    _status.CounterName = "PRD3"; 
    _status.Counters.Add(
 new Common.CPUPerformanceCounter(localMachineName)
 ); 
    _status.Counters.Add(
 new Common.ProcessorQueueLengthPerformanceCounter(localMachineName)
 ); 

    this.DataContext = _status; 

    _timer = new DispatcherTimer(); 
    _timer.Interval = new TimeSpan(0,0,1); 
    _timer.Tick += timer_Task; 
    _timer.Start(); 
} 

private void timer_Task(object sender, EventArgs e) 
{ 
    _status.Update(); 
} 

 

Other than the timer and the performance-counter code, this new demo works in exactly the same way as the previous one does. However, instead of reporting the values of a bunch of sliders, this new application lifts performance data from the local system. It is much more realistic than using sliders.

 

Download the code

SignalRDashboard2.zip