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;
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();
}
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"]; if (null !=device)
{
space = objMgmt["FreeSpace"]; if (null!=space)
diskSpace += device.ToString() +FormatBytes
(double.Parse(space.ToString())) +", ";
}
}
return diskSpace.Substring(0, diskSpace.Length-2);
}
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 <
and >
- Added
pen.Dispose();
to manually release it in time for this timer-driven application