Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Implementation of System Monitor

0.00/5 (No votes)
13 Sep 2007 1  
How to retrieve system data and draw data in light-weight custom chart controls
System Watcher Application from Zuoliu Ding

Google Desktop System Monitor

Introduction

System Watcher is a C# implementation of monitoring system data as shown in the first screenshot above. You have probably noticed an early version of Google Desktop System Monitor (second screenshot above) that was a gadget downloadable as a desktop plug-in. As a practice, I made System Watcher as an enhanced standalone tool to watch the PC performance. Typically, it shows CPU usage, virtual/physical memory usage, physical disk read/write BPS (bits per second) and network received/sent BPS. Using the discussion here, you can easily watch more system data in your applications.

Actually, I would like to use this example to demonstrate code in two aspects: first, how to collect the system data in your programming. Three simple methods involve retrieving Windows Management Instrumentation (WMI) objects, Performance Counters, and Environment Variables. To display data in the graphic format, I need some chart control. You may find a lot of sophisticated ones here and there. However, I decided to customize my own light-weight controls as an exercise, one for thick horizontal bars and the other for line charts and thin vertical sticks. I'll talk about the implementations in the following sections.

Collecting System Data

System data can be either dynamic or static. The dynamic data fluctuates with the current time, such as CPU and memory usage, disk and network throughput. Usually, you can collect the data from the Performance Counter object. For example, to get CPU usage, I first have:

PerformanceCounter _cpuCounter = new PerformanceCounter();

Then I call it in:

public string GetProcessorData()
{
    double d = GetCounterValue(_cpuCounter, 
        "Processor", "% Processor Time", "_Total");
    return _compactFormat? (int)d +"%": d.ToString("F") +"%";
}

...where GetCounterValue() is a general helper that I created to get a performance counter value:

double GetCounterValue(PerformanceCounter pc, string categoryName, 
    string counterName, string instanceName)
{
    pc.CategoryName = categoryName;
    pc.CounterName = counterName;
    pc.InstanceName = instanceName;
    return pc.NextValue();
}

Similarly, I can get the virtual memory percent and the physical disk read bytes like this:

d = GetCounterValue(_memoryCounter, "Memory", 
    "% Committed Bytes In Use", null);
d = GetCounterValue(_diskReadCounter, "PhysicalDisk", 
    "Disk Read Bytes/sec", "_Total");

As you can see in GetCounterValue(), I set the first CategoryName property as "PhysicalDisk" and set the second CounterName as "Disk Read Bytes/sec". For the third InstanceName property, it can be null in the case of "Memory" or a constant string like "_Total" for "PhysicalDisk".

However, for some performance counters, it may have multiple instances that depend on your computer configurations. An example is the "Network Interface" category, for which I found three instances on my computer (you can simply verify your system categories, counters and instances with the Server Explorer view in Visual Studio):
  • Broadcom NetXtreme 57xx Gigabit Controller
  • Intel[R] PRO_Wireless 2200BG Network Connection, and
  • Microsoft TCP Loopback interface

So, I first have to get all the instance names and save them in _instanceNames:

PerformanceCounterCategory cat = new PerformanceCounterCategory
    ("Network Interface");
_instanceNames = cat.GetInstanceNames();

If necessary, you can show the different counter values for an individual network instance. While in my System Watcher, I only need to get the sum of all counter values for bytes sent and received. The function to get network data would be like this:

public double GetNetData(NetData nd)
{
    if (_instanceNames.Length==0)
        return 0;

    double d =0;
    for (int i=0; i<_instanceNames.Length; i++)
    {
        d += nd==NetData.Received?
                GetCounterValue(_netRecvCounters[i], "Network Interface", 
                "Bytes Received/sec", _instanceNames[i]):
             nd==NetData.Sent?
                GetCounterValue(_netSentCounters[i], "Network Interface", 
                "Bytes Sent/sec", _instanceNames[i]):
             nd==NetData.ReceivedAndSent?
                GetCounterValue(_netRecvCounters[i], "Network Interface", 
                "Bytes Received/sec", _instanceNames[i]) +
                GetCounterValue(_netSentCounters[i], "Network Interface", 
                "Bytes Sent/sec", _instanceNames[i]):
                0;
    }

    return d;
}

...where NetData is defined as an enumeration type with ReceivedAndSent, Received and Sent. Using ReceivedAndSent, you add bytes received and sent together, which can be used in a compact format like the mini-pane in Google Desktop where the System Monitor is shrunk into the small sidebar. This is why I scatter the _compactFormat flag in my code.

The static system data can be retrieved from WMI that contains many classes for you to query, such as memory and disk space, computer and user information. Among them, Win32_ComputerSystem is important here and I created a common shared function for convenience:

public string QueryComputerSystem(string type)
{
    string str = null;
    ManagementObjectSearcher objCS = new ManagementObjectSearcher
        ("SELECT * FROM Win32_ComputerSystem");
    foreach ( ManagementObject objMgmt in objCS.Get() )
    {
        str = objMgmt[type].ToString();
    }
    return str;
}

I call it to get the computer manufacturer, model and user name, assuming sd is an object of the class containing QueryComputerSystem():

labelModel.Text = sd.QueryComputerSystem("manufacturer") +", " + 
    sd.QueryComputerSystem("model");
labelNames.Text = "User: " +sd.QueryComputerSystem("username");

To get physical memory usage, I have to use both WMI query and Performance counter objects:

public string GetMemoryPData()
{
    string s = QueryComputerSystem("totalphysicalmemory");
    double totalphysicalmemory = Convert.ToDouble(s);

    double d = GetCounterValue(_memoryCounter, "Memory", 
        "Available Bytes", null);
    d = totalphysicalmemory - d;

    s = _compactFormat? "%": "% (" + FormatBytes(d) +" / " +
        FormatBytes(totalphysicalmemory) +")";
    d /= totalphysicalmemory;
    d *= 100;
    return _compactFormat? (int)d +s: d.ToString("F") + s;
}

Another way to get static system data is to directly call ExpandEnvironmentVariables(). For instance, to get the processor identifier, call:

textBoxProcessor.Text = Environment.ExpandEnvironmentVariables
    ("%PROCESSOR_IDENTIFIER%");

Data Chart Custom Control

As I mentioned, two custom light-weight chart control classes are DataBar for the thick horizontal bar and DataChart for the thin line or stick chart. As DataBar is pretty trivial, I only talk about DataChart that is used as line charts in CPU and memory usage history, and also used as stick charts in Disk and Net throughput display. For example, to update CPU usage, I call:

string s = sd.GetProcessorData();
labelCpu.Text = s;
double d = double.Parse(s.Substring(0, s.IndexOf("%")));
dataBarCPU.Value = (int)d;
dataChartCPU.UpdateChart(d);

...where dataBarCPU is a DataBar object and dataChartCPU is a DataChart object. The only public method in DataChart is defined as follows:

public void UpdateChart(double d)
{
    Rectangle rt = this.ClientRectangle;
    int dataCount = rt.Width/2;

    if (_arrayList.Count >= dataCount)
        _arrayList.RemoveAt(0);

    _arrayList.Add(d);
    Invalidate();
}

In this method, I first calculate the maximum data count in pixels based on the client rectangle width, assuming that each value in the drawing occupies two pixels. Then I add the new value to the internal storage _arrayList and make it as a circular queue. Finally, I invalidate the control to invoke virtual OnPaint().

To designate the chart drawing format, I created five properties. LineColor and GridColor are two drawing colors. GridPixels is the pixel spacing in a grid or set zero not to draw the grid. InitialHeight is the maximum logical value estimated at the beginning. It will be helpful to adjust the vertical ratio initially. ChartType is an enumeration with Line for lines and Stick for sticks in the chart.

The following properties are set for dataChartCPU and dataChartDiskR respectively in System Watcher.

Now we can take a look at the essential job that I override, namely OnPaint():

protected override void OnPaint(PaintEventArgs e)
{
    int count = _arrayList.Count;
    if (count==0) return;

    double y=0, yMax = InitialHeight;
    for (int i=0; i<count; i++)
    {
        y = Convert.ToDouble(_arrayList[i]);
        if (y>yMax) yMax = y;
    }

    Rectangle rt = this.ClientRectangle;
    y = yMax==0? 1: rt.Height/yMax;        // y ratio

    int xStart = rt.Width;
    int yStart = rt.Height;
    int nX, nY;

    Pen pen = null;
    e.Graphics.Clear(BackColor);

    if (GridPixels!=0)
    {
        pen = new Pen(GridColor, 1);
        nX = rt.Width/GridPixels;
        nY = rt.Height/GridPixels;

        for (int i=1; i<=nX; i++)
            e.Graphics.DrawLine(pen, i*GridPixels, 0, i*GridPixels, yStart);

        for (int i=1; i<nY; i++)
            e.Graphics.DrawLine(pen, 0, i*GridPixels, xStart, i*GridPixels);

        pen.Dispose();
    }
    
    // From the most recent data, draw from right to left    
    // Get data from _arrayList[count-1] to _arrayList[0]  
    if (ChartType==ChartType.Stick)
    {    
        pen = new Pen(LineColor, 2);

        for (int i=count-1; i>=0; i--)
        {
            nX = xStart - 2*(count-i);
            if (nX<=0) break;

            nY = (int)(yStart-y*Convert.ToDouble(_arrayList[i]));
            e.Graphics.DrawLine(pen, nX, yStart, nX, nY);
        }
        
        pen.Dispose();
    }
    else
    if (ChartType==ChartType.Line)
    {
        pen = new Pen(LineColor, 1);

        int nX0 = xStart - 2;
        int nY0 = (int)(yStart-y*Convert.ToDouble(_arrayList[count-1]));
        for (int i=count-2; i>=0; i--)
        {
            nX = xStart - 2*(count-i);
            if (nX<=0) break;

            nY = (int)(yStart-y*Convert.ToDouble(_arrayList[i]));
            e.Graphics.DrawLine(pen, nX0, nY0, nX, nY);

            nX0 = nX;
            nY0 = nY;
        }
        
        pen.Dispose();
    }

    base.OnPaint(e);
}

The drawing procedure is straightforward. I first search for the largest logical value yMax from the data in _arrayList. Then I save the calculated vertical ratio for subsequent drawing. If GridPixels is non-zero, I draw the grid with GridColor and GridPixels. At last, I represent the data, either lines or sticks, according to the ChartType property value.

Points of Interest

If you want to make use of the DataChart class, you may add your properties to make it more flexible, such as the line/stick width and the stick interval. Also, you can draw x and y coordinates with logical values. Any enhancement will make DataChart nice and easy to use.

For the system data, I tried to add a free space watch for all logic disks. What I expected was to get back a string like "C:10.32 GB, D:5.87GB". To achieve this, I wrote the following function with the WMI call to the Win32_LogicalDisk class:

public string LogicalDisk()
{
    string diskSpace = string.Empty;
    object device, space;
    ManagementObjectSearcher objCS = new ManagementObjectSearcher
        ("SELECT * FROM Win32_LogicalDisk");
    foreach ( ManagementObject objMgmt in objCS.Get() )
    {
        device = objMgmt["DeviceID"];        // C:
        if (null !=device)
        {
            space = objMgmt["FreeSpace"];    // C:10.32 GB
            if (null!=space)
                diskSpace += device.ToString() +FormatBytes
                (double.Parse(space.ToString())) +", ";
        }
    }

    return diskSpace.Substring(0, diskSpace.Length-2);  
        // C:10.32 GB, D:5.87GB
}

It works fine in most cases, with Ethernet connected LAN or offline. Unfortunately, when I switched to a wireless network connection, LogicalDisk() suffered a long delay at objCS.Get() of about 20 seconds that froze the main thread. I tried some way around it without results. Passing a delegate as a callback or creating a work thread would not help to solve this lengthy blocking issue at the time of this writing. Any comments and suggestions are welcome.

History

  • 7 September, 2007 -- Original version posted
  • 13 September, 2007 -- Article updated:
    • In OnPaint(), corrected some '<' and '>' to &lt; and &gt;
    • Added pen.Dispose(); to manually release it in time for this timer-driven application

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here