How's My Driving? Monitoring Performance Using WMI

 

Greg Stemp
Microsoft Corporation

November 10, 2003

Summary: WMI provides a full set of easy-to-use performance counter classes. Learn how to create monitoring scripts that take advantage of these classes to monitor processor use, memory use, network connections, disk use, Web service performance, server connections, printer use, and more. (23 printed pages)

Hello everyone; long time no see, huh? I apologize for our lengthy absence from this space, but—to be perfectly honest—we Scripting Guys have a lot of other important duties, duties that take priority over scripting work, duties that require setting aside frivolous pursuits like the Scripting Clinic, and instead focusing on something that will truly benefit Microsoft. You didn't think Bill Gates' car washes itself, did you?

Well, actually, Bill's car does wash itself. But most of the other Microsoft executives don't have cars quite that fancy. As it turns out, though, you can only wash cars so many times. And with the weather growing colder and the grass no longer growing as quickly, it's time once again to sit down at the computer and hammer out a brand-new Scripting Clinic.

Before I do that, however, I need to make sure that everyone reading this column is familiar with the story of Frankenstein. Now, no doubt all of you have read the book (which means, of course, that you've all seen the movie), but just in case, here's a quick recap: An ambitious young scientist named Victor Frankenstein decides to gather up a bunch of dead bodies, take the best parts from each, sew those parts together, and then bring this creature to life, creating a superhuman super being. (Incidentally, this is pretty much the same process by which the original four Scripting Guys added Peter Costantini to the team.) Things seemed to go pretty well at first, but you know how it is when you bring the dead back to life: no matter how good things start out, they rapidly deteriorate. To make a long story short, things rapidly deteriorated and, in the end, pretty much everybody died.

Of course, the moral to Frankenstein is clear: There are some things people simply are not meant to meddle with; foremost among those being the idea of bringing the dead back to life. (Or, at the least, if you're going to bring the dead back to life, start with something small and easy to control, like a gerbil or a hamster.) Frankenstein has a lesson for all of us: Some things are better left undone, a point I try to make every time I'm handed a new assignment.

Anyway, why am I talking about Frankenstein when I could be busy sweeping the sidewalk, fetching coffee, or otherwise making myself useful? Well, few people are aware of this, but Mary Shelley actually started, but never finished, a sequel to Frankenstein. This one had the same moral—there some things people are not meant to meddle with—but instead of bringing the dead back to life, Shelley turned to a far more frightening prospect: writing WMI scripts that monitor performance. Scholars believe eventually Shelley abandoned the book because she felt that no one would be able to handle the sheer horror of it all.

**Literary coincidence   **Did you know that Mary Shelley was only 19 when she wrote Frankenstein? That's right, the exact same age as the each one of the Microsoft Scripting Guys!

Because Shelley couldn't bear to finish her book, it's likely that many of you aren't even aware that you can monitor performance using WMI. It's true though. Beginning with Microsoft® Windows® 2000, Microsoft added the so-called Win32_PerfRawData performance monitoring classes to WMI. What do these Win32_PerfRawData let you do? A better question would be: what don't they let you do? Using the Win32_PerfRawData classes, you can write scripts that do all the things Performance Monitor does. What do you want to monitor: processor use, memory use, network connections, disk use, Web service performance, server connections, printer use? The Win32_PerfRawData classes let you monitor all these things, and more. And because these are WMI classes, they are readily available to script writers. Very cool stuff indeed.

**Note   **What this means is that the scripts we are about to show you will work on Windows 2000, Windows XP, and Microsoft® Windows Server™ 2003. However, they won't work on Windows NT 4.0 or Windows 98. Sorry.

So if performance monitoring through WMI is so great, why hasn't anyone ever heard of it? Why isn't the Script Center overflowing with sample performance monitoring scripts? Why isn't anyone using this technology? Well, let's put it this way: Bringing the dead back to life sounded like a good idea, but once Victor Frankenstein tried it, he found out there were a number of unforeseen consequences. The same is true here: People who have tried writing WMI scripts to monitor performance have discovered a number of unforeseen consequences.

Let's take a classic example—using a performance monitoring class that allows us to determine the percentage of free disk space on a disk drive. Here's a script that would appear to return the percentage of free disk space on a disk drive:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_PerfRawData_PerfDisk_LogicalDisk " _
        & "Where Name = 'C:'")
For Each objDisk in colDisks
    Wscript.Echo objDisk.PercentFreeSpace & "% free space."
Next

So what happens if we run this on a computer with 4.2 gigabytes of free disk space on an 11.5-gigabyte hard disk? An off-the-top-of-your-head calculation suggests that approximately one-third of the disk (33 percent) is unused, but here's what the script reports:

Figure 1. Incorrectly reporting the percent of free disk space

Wow, 4145 percent free disk space! And all this time you thought 100 percent was the highest percentage you could get. (Kind of makes that 92 percent you got in high school chemistry look a little lame, doesn't it?)

But don't get too excited; you haven't discovered some cool new disk compression method. As it turns out, the Win32_PerfRawData classes are just what the name implies: they are "raw" data classes, which means that the values returned aren't necessarily the final values. Instead, they often have to be "cooked;" that is, you have to run the number you get back through some sort of mathematical formula in order to obtain the real value. In this case, we need to take our returned value of 4145, multiply it by 100, and divide it by 11507 in order to get the actual percentage of free disk space—a much more mundane 36 percent.

**Note   **Don't worry; we'll explain where all these numbers came from.

Now, if all performance counters used the same mathematical formula, maybe this wouldn't be so bad; you could write a function, run the returned data through that function, and then report the "cooked" results. A bit of a hassle, sure, but no big deal.

As it turns out, however, all performance counters are not created equal: There are a number of different performance counter types available to software developers, and these different counter types all seem to use different mathematical formulas. In other words, you could—at least in theory—find yourself writing 30 functions in each script, depending on what you were trying to monitor. It's a very daunting prospect—so daunting that most people give up before they even get started.

But there's something most people don't know. In the book, Frankenstein's monster wasn't really a monster. Instead, everyone believed he was a monster, and they allowed this belief to color their behavior. Well, the same thing is true of performance monitoring using WMI. It's really not all that bad; it's just that everyone believes it's that bad, and they allow this belief to color their behavior. Here's the truth: Monitoring performance using WMI isn't all that hard. In fact, by the time you get to the end of this column, you'll be able to write scripts that take full advantage of the Win32_PerfRawData classes—guaranteed.

**Note   **Guarantee not necessarily guaranteed.

But wait, you say; I thought there were like 900 million different performance counter types available to software developers? Well, that's true (give or take a few hundred million). However, only a handful of those types are ever actually used. In fact, you can simply ignore the vast majority of counter types. Don't believe us? We took two computers—a Windows XP Professional machine and a Windows Server 2003 machine—and used a script (which we'll show you later) to retrieve the individual performance counters and their type from the various Win32_PerfRawData classes. Here's a summary of what we found:

Counter Type Instances on XP Instances on 2003
PERF_COUNTER_RAWCOUNT 475 750
PERF_COUNTER_COUNTER 211 320
PERF_COUNTER_LARGE_RAWCOUNT 97 122
PERF_COUNTER_BULK_COUNT 63 78
PERF_RAW_FRACTION 13 30
PERF_100NSEC_TIMER 23 23
PERF_PRECISION_100NS_TIMER 8 8
PERF_AVERAGE_BULK 6 6
PERF_AVERAGE_TIMER 6 6
PERF_COUNTER_100NS_QUELEN_TYPE 6 6
PERF_SAMPLE_FRACTION 0 5
PERF_ELAPSED_TIME 4 4
PERF_COUNTER_TIMER 0 2
PERF_100NSEC_TIMER_INV 1 1
PERF_COUNTER_RAWCOUNT_HEX 1 1
PERF_COUNTER_LARGE_RAWCOUNT_HEX 1 1
PERF_COUNTER_LARGE_QUELEN_TYPE 0 0
PERF_COUNTER_TIMER_INV 0 0
PERF_COUNTER_TEXT 0 0
PERF_COUNTER_MULTI_TIMER_INV 0 0
PERF_COUNTER_DELTA 0 0
PERF_COUNTER_LARGE_DELTA 0 0
PERF_SAMPLE_COUNTER 0 0
PERF_COUNTER_QUELEN_TYPE 0 0
PERF_PRECISION_SYSTEM_TIMER 0 0
PERF_OBJ_TIME_TIMER 0 0
PERF_COUNTER_MULTI_TIMER 0 0
PERF_100NSEC_MULTI_TIMER 0 0
PERF_100NSEC_MULTI_TIMER_INV 0 0
PERF_COUNTER_OBJ_TIME_QUELEN_TYPE 0 0

If you take a look at the table, you'll see something interesting: six counter types (PERF_COUNTER_RAWCOUNT, PERF_COUNTER_COUNTER, PERF_COUNTER_LARGE_RAWCOUNT, PERF_COUNTER_BULK_COUNT, PERF_RAW_FRACTION, and PERF_100NSEC_TIMER) account for almost all the performance counters used on Windows XP and Windows Server 2003. (You might notice as well that many valid counter types aren't even used.) On Windows XP, 96% of the performance counters fall into one of these six types; on Windows Server 2003, over 97% of the counters belong to one of these six. In other words, you can monitor the performance of almost anything just by learning how to work with six counter types. It's so easy, even a Scripting Guy could do it!

In fact, it's even easier than that. Two of the most commonly used types—PERF_COUNTER_RAWCOUNT and PERF_COUNTER_LARGE_RAWCOUNT—require no calculations whatsoever; you can just use the returned values as is. Two of the others—PERF_COUNTER_COUNTER and PERF_COUNTER_BULK_COUNT—use the same formula. Now we're down to just three formulas you have to learn. At this rate, by the time you finish this article, you'll be able to monitor performance without knowing anything.

To make things even easier, we'll show you how to write scripts using each of these counter types. Then we'll show you how to determine the counter type for a given performance counter. At that point, you'll be ready to bring the dead back to life. Or write performance-monitoring scripts. Like they say, it's all good.

One Word of Caution

Before we start, there's an important point we need to make, one that holds true regardless of the performance counter type you are using. Below is a script that looks simple enough. It connects to the Win32_PerfRawData_PerfOS_Memory class and then echoes the value of the AvailableMbytes counter. (We chose AvailableMbytes for this demonstration because it's a counter that requires no cooking.) The script pauses for 5 seconds, then echoes the value of AvailableMbytes again, and again, and again, until it's taken 20 such measurements.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfRawData_PerfOS_Memory")
For i = 1 to 20
    For Each objItem in colItems
        intValue = objItem.AvailableMbytes
        Wscript.Echo "Available memory = " & intValue & " megabytes"
        Wscript.Sleep(5000)
    Next
Next

Let's say you run this script. On the first iteration, if you have 53 megabytes of available memory, the script will correctly report that fact. On the second iteration, the script will also report 53 megabytes of available memory. In fact, on every iteration it will report 53 megabytes of available memory. You can start programs or stop programs; the script will still report 53 megabytes of available memory. You can start services, stop services, copy Windows Media files over the network, you can even take the cover off your computer and pull out all the RAM chips; the script will still report 53 megabytes of available memory. Do whatever you want, and the script will stubbornly insist that the computer has 53 megabytes of available memory.

Why? Well, take a careful look at the boldface lines in the previous script. You'll notice that, early on, we retrieve all the instances of the Win32_PerfRawData_PerfOS_Memory class. In effect, this takes a snapshot of the Win32_PerfRawData_PerfOS_Memory property values at the time we ran ExecQuery(you can tell that's important, because we put it in italics). We then set up a For-Next loop to loop 20 times; during each loop, we report the value of the AvailableMbytes property.

That might seem okay, but we made a crucial mistake here: We only ran ExecQuery once, meaning we took a single snapshot of the memory performance counters. After that, we never took another snapshot; we never retrieved a new set of values. Instead, we have one snapshot, and we simply keep showing that one snapshot over and over again. Within our For-Next loop, we're echoing the value of AvailableMbytes, but it's the same value each time; we never do anything to update it. If we got 53 on the first go-round, the script will report 53 each time, because we haven't gone out and retrieved the latest set of data. We're just showing the same thing over and over and over again. Just like network television.

If that's a bit confusing, it might help to look at a script that correctly reports available memory on each iteration. Notice the location of the ExecQuery method in this script: it's inside the For-Next loop. That means we'll run ExecQuery 20 times (one for each iteration of the loop). Because of that, each time we run through the loop, we'll get the current value of AvailableMbytes (that is, a brand-new snapshot). In turn, each time we'll echo the real amount of available memory, not merely the amount of memory that was available when we first started the script.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
For i = 1 to 20
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_PerfRawData_PerfOS_Memory")
    For Each objItem in colItems
        intValue = objItem.AvailableMbytes
        Wscript.Echo "Available memory = " & intValue & " megabytes"
    Next
    Wscript.Sleep(5000)
Next

The best way to see how this works is to try it for yourself. Copy these two scripts and run them on your computer. As the scripts run, open and close a bunch of programs. You'll see that the amount of available memory will change when you run the second script, but that it will not change when you run the first script. The moral of this story: make sure your performance monitoring scripts are always retrieving the current set of performance values. And to do that, run ExecQuery each time you want to echo a new set of values.

**Admission   **Yes, this can be a hassle, and it can be hard to keep track of if you're gathering performance counters from multiple classes (something we'll discuss later). In Windows XP and Windows 2003, there's a new WMI object—the SWbemRefresher object—that takes care of all this for you. You simply tell SwbemRefresher what counters you want to keep track of, and then use a single line of code (which calls the object's Refresh method) any time you want to retrieve the latest set of values for all the performance counters in your script. In a future column we'll tell you how to do this.

Now let's take a quick look at the six most-commonly used counter types.

PERF_COUNTER_RAWCOUNT

Over half the performance counters available to you are of this type, and that's good, because these counters require no mathematical calculations, voodoo curses, or any other kind of magical transformation. You simply retrieve the value and use it as is. If this script reports that Microsoft® Word has 916 handles open, then call your bookie, because you can bet that Word really does have 916 handles open.

**Note   **Please don't call your bookie. I can't in good conscience endorse gambling, especially after I put $5000 down on the Cubs to win the World Series.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
For i = 1 to 5
    Wscript.Sleep(1000)
    Set colItems = objWMIService.ExecQuery _
       ("Select * From Win32_PerfRawData_PerfProc_Process Where " & _
            "Name = 'WINWORD'")
    For Each objItem in colItems
        intValue = objItem.HandleCount
        Wscript.Echo "Handle Count = " & intValue
    Next
Next

If you're thinking, "Hmmm, this looks like a regular old WMI script to me," well, guess what: you're absolutely right. We connect to the WMI service, get the data from the Win32_PerfRawData_PerfProc_Process class, and then echo the property value HandleCount. The only difference between this and most WMI scripts is the fact that we perform all this work within a loop; that way, we can echo the handle count more than once. (A handle count that rises steadily without ever falling is often an indication of a memory leak.)

**Note   **In the preceding script, we were only interested in what Word was up to, so we included the Where clause, Where Name = 'WINWORD'. That way, the script only reports process data for Winword.exe. What if we wanted similar performance data for all of the processes currently running on a computer? No problem; just leave out the Where clause:

Set colItems = objWMIService.ExecQuery _
       ("Select * From Win32_PerfRawData_PerfProc_Process")

Of course, if you do this, you should probably echo the process name along with the handle count. That way you can match a handle count with a process.

PERF_COUNTER_LARGE_RAWCOUNT

Here's the other counter type that can be used as is; you just retrieve the value and report it. What's so scary about that?

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
For i = 1 to 5
    Wscript.Sleep(1000)
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_PerfRawData_PerfProc_Process Where " & _
            "Name = 'WINWORD'")
    For Each objItem in colItems
        intValue = objItem.WorkingSet
        Wscript.Echo "Working Set = " & intValue
    Next
Next

PERF_RAW_FRACTION

So far, so good. We've been able to get a lot useful performance information without all that much effort. With PERF_RAW_FRACTION, however, the party's over; we have to make our first calculation. But, come on, what's so hard about this?

(100 * CounterValue) / BaseValue

Ok, what's hard is knowing what the variables represent. So, we'll tell you. In this formula, CounterValue equals the value of the performance counter, and BaseValue—well, BaseValue is just a number used to make the calculation come out right. To be honest, we don't know exactly how the BaseValue is derived. Fortunately, we don't need to know how it's derived as long as we can figure out what the value is—something we'll show you how to do in a second.

Here's what the formula looks like in an actual script: We connect to the WMI service, get the data from the Win32_PerfRawData_PerfDisk_LogicalDisk class, get the value of the PercentFreeSpace counter, multiply that by 100, and then divide the answer by the base value (11507).

intBaseValue = 11507
strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_PerfRawData_PerfDisk_LogicalDisk Where Name = 'C:'")
For Each objDisk in colDisks
    dblActualFreeSpace = (100 * objDisk.PercentFreeSpace) / intBaseValue
    Wscript.Echo Int(dblActualFreeSpace)
Next

That sounds like a lot of work, but it's really not all that bad. Let's say PercentFreeSpace returned a value of 4145. In that case, the calculation would look like this:

(100 * 4145) / 11507

Or:

414500 / 11507 = 36

Actually, this is remarkably easy except for one thing: How did I know that the base value for PercentFreeSpace is 11507? I looked it up. Any time you have a PERF_RAW_FRACTION performance counter, you will also have an accompanying base-value performance counter. To find out what that counter is, just open up Wbemtest and take a look:

Figure 2. Locating a base value performance counter in Wbemtest

So then how do we get the value for PercentFreeSpace_Base? Click the Instances button in Wbemtest, and then double-click any of the resulting instances (it doesn't matter which one). The value for PercentFreeSpace_Base is the base value you'll use in your scripts.

Figure 3. Determining the value of a base value performance counter

There's only one thing you need to be careful of here: The value of PercentFreeSpace_Base can vary depending on your hardware, your operating system, the phases of the moon, and so on. (Well, actually, it depends on the size of your logical disk.) Because of that, you shouldn't hardcode in a number like 11507 (this will only work for a disk of a certain size), but instead use the retrieved value of PercentFreeSpace_Base. Note the boldfaced line in the following script.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_PerfRawData_PerfDisk_LogicalDisk Where Name = 'C:'")
For Each objDisk in colDisks
    intBaseValue = objDisk.PercentFreeSpace_Base
    dblActualFreeSpace = (100 * objDisk.PercentFreeSpace) / intBaseValue
    Wscript.Echo Int(dblActualFreeSpace)
Next

**Tip   **How do you know if your scripts are returning the correct data? Here's one quick way to double-check your work. Start up Performance Monitor and load in the performance counters you're running in your script. Run the script at the same time, and visually compare your values with the Performance Monitor values. That will help you know whether or not you're on the right track.

PERF_COUNTER_COUNTER

Still with us? Good. Now, this next one might seem a bit tricky, but don't panic. Take a deep breath, remain calm, and you'll see that it's nowhere near as bad as you might first think.

PERF_COUNTER_COUNTER (no relation to Boutros Boutros-Ghali) is designed to measure the number of events (the definition of "event" depends on the individual counter) that happen per second. Let's forget about performance monitoring for a moment (some of you might have done that several pages ago), and think about how you would measure the number of anythings per second. It's actually pretty easy.

  • You figure out how many events occurred.
  • You figure out how much time elapsed.
  • You convert the elapsed time to seconds. For example, if the elapsed time was 2 minutes, you need to multiply by 60 to convert 2 minutes to 120 seconds.

The same basic idea works with PERF_COUNTER_COUNTER:

  • You take two measurements (CounterValue1 and CounterValue2), and then figure out how many things happened between those two endpoints (CounterValue2 minus CounterValue1).
  • You calculate the elapsed time by taking the time when you started measuring (TimeValue1) and then subtracting that from the time when you finished measuring (TimeValue2 minus TimeValue1).
  • You divide by the time base to convert the elapsed time to seconds (the operating system typically measures events in nanoseconds).

Here's the actual formula for PERF_COUNTER_COUNTER:

(CounterValue2 - CounterValue1) / ((TimeValue2 - TimeValue1) / TimeBase)

Yes, we know it looks scary, but keep this in mind: We only have 5 variables in this formula, and we're going to take care of three of those right away. Take a look at the script below. See the three boldfaced lines? Here we've queried the Win32_PerfRawData_PerfOS_Processor class and retrieved three property values:

  • InterruptsPerSec. This is the PERF_COUNTER_COUNTER value we want to measure. We'll get a value here that will serve as CounterValue1 in our equation.
  • TimeStamp_PerfTime. This is our starting time (in other words, TimeValue1). How did we know to use this as our starting time? We'll explain that later.
  • Frequency_PerfTime. This is our TimeBase. Basically, it's a value used to make sure our time comes out in seconds (because we are measuring interrupts per second). Again, we'll explain where we got this from momentarily.
strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfRawData_PerfOS_Processor Where Name = '0'")
For Each objItem in colItems
    CounterValue1 = objItem.InterruptsPerSec
    TimeValue1 = objItem.TimeStamp_PerfTime
    TimeBase = objItem.Frequency_PerfTime
Next
For i = 1 to 5
    Wscript.Sleep(1000)
    Set colItems = objWMIService.ExecQuery _
       ("Select * From Win32_PerfRawData_PerfOS_Processor Where Name = '0'")
    For Each objItem in colItems
        CounterValue2 = objItem.InterruptsPerSec
        TimeValue2 = objItem.TimeStamp_PerfTime
        If TimeValue2 - TimeValue1 = 0 Then
            Wscript.Echo "Interrupts Per Second = 0"
        Else
            intInterrupts = (CounterValue2 - CounterValue1) / _
                ( (TimeValue2 - TimeValue1) / TimeBase)
            Wscript.Echo "Interrupts Per Second = " & Int(intInterrupts)
        End if
        CounterValue1 = CounterValue2
        TimeValue1 = TimeValue2
    Next
Next

In other words, a few lines of code, and we've already figured out three of our five variables:

(CounterValue2 - CounterValue1) / ((TimeValue2 - TimeValue1) / TimeBase)

So how do we get the other two variables? It's easy. We create another For-Next loop and make an ExecQuery call, just like we did in our previous scripts. We then get the current value of InterruptsPerSec, assigning that to CounterValue2, and the currentTimeStamp_PerfTime, assigning that to TimeValue2. (We don't need to retrieve Frequency_PerfTime because that value remains constant.) Now we have all our variables:

(InterruptsPerSec No. 2 - InterruptsPerSec No. 1 ) / _
    ((TimeStamp_PerfTime No. 2 - TimeStamp_PerfTime No. 1)_
         / Frequency_PerfTime

That's basically all there is to it. If we were taking only a single measurement, we could run the formula, echo the result, and be done with this script. However, since we're taking several measurements, we need to do one other thing. In the two boldfaced lines near the end of the script, we assign the value of our second set of counter data to CounterValue1 and the second timestamp to TimeValue2. Why? Well, suppose our counter value readings look like this:

CounterValue1: 14

CounterValue2: 25

By subtracting 14 from 25, we know 11 interrupts occurred during our measurement interval. Suppose we now decide to measure interrupts again. We don't want CounterValue1 to be equal to 14; that information is old and outdated. For the next round of measurements, we aren't starting at 14, we're starting at 25. So, we assign 25 (the value of CounterValue2) to CounterValue1. When we take our next measurement, CounterValue2 might be, say, 37, so our formula will be 37-25, or 12 interrupts. Make sense? We do the same thing with the timestamp to make sure we have the correct time interval as well.

PERF_COUNTER_BULK_COUNT

Okay, calculating the value of PERF_COUNTER_COUNTER wasn't all that much fun. (Or as Frankenstein's creature put it: "Everywhere I see bliss, from which I alone am irrevocably excluded." Man, no wonder the producers decided not to let him speak in the movie!)

On the bright side, though, we did kill two birds with one stone: After all, once you know how to calculate PERF_COUNTER_COUNTER, you also know how to calculate PERF_COUNTER_BULK_COUNT. The two use the same formula.

**Note to animal rights activists   **No birds were actually killed in the writing of this column, although our editor repeatedly complained of a queasy feeling in his stomach each time he saw a quotation from Frankenstein.

Skeptical? Take a look at this PERF_COUNTER_BULK_COUNT script. See the items in bold? Those class-specific changes represent the only differences between this script and the PER_COUNTER_COUNTER script.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfRawData_PerfOS_System")
For Each objItem in colItems
    CounterValue1 = objItem.FileControlBytesPerSec
    TimeValue1 = objItem.TimeStamp_PerfTime
    TimeBase = objItem.Frequency_PerfTime
Next
For i = 1 to 5
    Wscript.Sleep(1000)
    Set colitems = objWMIService.ExecQuery _
        ("Select * From Win32_PerfRawData_PerfOS_System")
    For Each objItem in colItems
        CounterValue2 = objItem.FileControlBytesPerSec
        TimeValue2 = objItem.TimeStamp_PerfTime
        If TimeValue2 - TimeValue1 = 0 Then
            Wscript.Echo "File Control Bytes Per Second = 0"
        Else
            intInterrupts = (CounterValue2 - CounterValue1) / _
                ( (TimeValue2 - TimeValue1) / TimeBase)
            Wscript.Echo "File Control Bytes Per Second = " & intInterrupts
        End if
        CounterValue1 = CounterValue2
        TimeValue1 = TimeValue2
    Next
Next

**Tip   **Are you thinking what we're thinking? Are you thinking that the sample scripts in this column would make excellent templates for your own performance-monitoring scripts? Are you thinking you could easily create your own scripts just by copying these scripts and changing a class-specific property here and there? Well, that isn't what we were thinking, but that's a pretty good idea nonetheless.

PERF_100NSEC_TIMER

Ah, PERF_100NSEC_TIMER—the last, but surely not the least, of our most commonly used performance counter types. The formula for this counter type isn't really all that bad (only 4 variables as opposed to 5:

100* (CounterValue2 - CounterValue1) / (TimeValue2 - TimeValue1)

In fact, the only differences between this counter type formula and the last counter type formula we showed you are that:

  • You don't have to worry about the time base here; it isn't used in the calculation.
  • Instead of using the TimeStamp_PerfTime property to determine the time interval, you use TimeStamp_Sys100NS instead.

Okay, that last bullet point might have you a bit concerned. How do you know when to use TimeStamp_PerfTime and when to use TimeStamp_Sys100NS? Well, we'll give you a few pointers on that momentarily. But remember, as soon as you know the counter type, you know which time interval property to use. If you have a PERF_100NSEC_TIMER counter, you will always use TimeStamp_Sys100NS.

Why? There's actually a simple reason: If you didn't use TimeStamp_Sys100NS, then, by definition, you wouldn't have a PERF_100NSEC_TIMER counter.

True, that's kind of a frivolous explanation, but that's also the kind of thing you shouldn't worry about. Whoever invented the PERF_100NSEC_TIMER counter type used TimeStamp_Sys100NS in the formula, which means you will always use TimeStamp_Sys100NS when dealing with this counter type. Why do they use / to indicate division rather than \ ? Who knows? But it doesn't matter, because any time you want to divide something you use /. It's one of the immutable laws of nature. The same is true with performance counter types.

Here's a sample of PERF_100NSEC_TIMER in action. Again, the boldface items represent the only items that have to change if you decide to modify this script and retrieve the value of a different performance counter.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfRawData_PerfOS_Processor Where Name = '0'")
For Each objItem in colItems
    CounterValue1 = objItem.PercentUserTime
    TimeValue1 = objItem.TimeStamp_Sys100NS
Next
For i = 1 to 5
    Wscript.Sleep(1000)
    Set colItems = objWMIService.ExecQuery _
       ("Select * From Win32_PerfRawData_PerfOS_Processor Where Name = '0'")
    For Each objItem in colItems
        CounterValue2 = objItem.PercentUserTime
        TimeValue2 = objItem.TimeStamp_Sys100NS
        If TimeValue2 - TimeValue1 = 0 Then
            Wscript.Echo "Percent User Time = 0%"
        Else
            PercentProcessorTime = 100 * (CounterValue2 - CounterValue1) / _
                (TimeValue2 - TimeValue1)
            Wscript.Echo "Percent User Time = " & _
                PercentProcessorTime & "%"
        End if
        CounterValue1 = CounterValue2
        TimeValue1 = TimeValue2
    Next
Next

Bonus Script: Retrieving Values from Multiple Classes

One thing we've tried to do in this column is to keep our examples as simple as possible. Because of that, we've shied away from something you're likely to have to do: retrieve performance counters from more than one Win32_PerfRawData class. For example, you might need to know both available memory (found in Win32_PerfRawData_PerfOS_Memory) and available disk space (Win32_PerfRawData_PerfDisk_LogicalDisk). Can you do this is in one script, or do you need to write separate scripts, one for each performance class?

As it turns out, you can do this in one script; you just have to have separate ExecQuery calls for each performance class. (Why? Because WMI doesn't allow you to write a query along the lines of "Select * From Win32_PerfRawData_PerfOS_Memory and * From Win32_PerfRawData_PerfDisk_LogicalDisk.")

Here's a sample script that retrieves performance counter values from two different classes. Notice the boldface lines; there are separate ExecQuery calls (one for each performance class) inside the For-Next loop.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
For i = 1 to 5
    Wscript.Sleep(5000)
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_PerfRawData_PerfOS_Memory")
    For Each objItem in colItems
        intValue = objItem.AvailableMbytes
        Wscript.Echo "Available memory = " & intValue & " megabytes"
    Next
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_PerfRawData_PerfDisk_LogicalDisk Where " & _
            "Name = 'C:'")
    For Each objItem in colItems
        intValue = objItem.FreeMegabytes
        Wscript.Echo "Available disk space = " & intValue & " megabytes"
    Next
Next

**A reason to upgrade   **In Windows XP and Windows Server 2003, there is a much cleaner, neater, and easier way to retrieve, and update, performance counters taken from multiple classes. Again, we'll talk about that in a future column.

How Do I Know Which Performance Counters Belongs to Which Type?

Okay. So now we've shown you the six basic counter types, and also shown how you can use each of those counter types in a script. That still leaves a major problem: If you're looking at the Win32_PerfRawData_PerfOS_Processor class and you see an interesting property like PercentProcessorTime, how do you know which of these six types (if any) that individual counter falls under?

Well, it takes some digging, but between the WMI SDK and MSDN, it's humanly possible to determine these formulas on your own. (Of course, it's humanly possible to climb Mt. Everest, too, but we don't necessarily recommend that you all rush out and try that.) Within the WMI SDK, you'll find information like this for each property in a Win32_PerfRawData class:

PercentProcessorTime

Data type: uint64
Access type: Read-only
Qualifiers: DisplayName("% Processor Time"), PerfDefault, CounterType(558957824), DefaultScale(0), PerfDetail(100)

For our purposes, we're interested in the value of the CounterType qualifier (in this case 558957824). If you search for 558957824 in the WMI SDK, you'll eventually discover that this is the decimal value that represents a PERF_100NSEC_TIMER_INV counter type. Armed with that information, you can then venture out to MSDN, where you'll find this exact table:

Element Value
X CounterData
Y 100NsTime
Time base 100Ns
Data Size 8 Bytes
Display Suffix %
Calculation 100*(1-(X1-X0)/(Y1-Y0))

How does that help you? Well, here's a quick way to interpret a table like this:

  • X will always represent the counter data. Always.
  • Y will always represent the time interval used. (Remember, not all counters will have a time interval.) Y will almost always be one of two values: 100NSTime or PerfTime. If Y is 100NSTime, use this property in your equation: TimeStamp_Sys100NS. If Y is PerfTime, use this property instead: TimeStamp_PerfTime.
  • Time base is the value used to convert the returned data to a standard unit (such as seconds). If the Time base is 100NS, use the property Frequency_Sys100NS in your calculations. If the time base is PerfFreq, use Frequency_PerfTime. Again, not all counters will have a time base.

So what does the formula 100*(1-(X1-X0)/(Y1-Y0)) translate to? Well, X is equal to our counter data (PercentProcessorTime). Y is equal to 100NsTime, which means we'll use the property TimeStamp_Sys100NS. And although the time base is 100Ns. It's not used in the formula, so we'll just ignore it. Thus:

100 * (1 - (PercentProcessorTime No. 2 - PercentProcessorTime No. 1) _
    / (TimeStamp_Sys100NS No. 2 - TimeStamp_Sys100NS No. 1))

A little crazy, but it works.

**Confession   **Yes, we purposely chose a performance counter that isn't one of the six main types. However, based on what you've already learned, you should be able to figure out the formula for PERF_100NSEC_TIMER_INV, meaning you've now mastered seven of the available counter types.

So is there a better way to get at this information? Let's hope so. Here's one suggestion. The following is a script that retrieves the names of all the Win32_PerfRawData counter classes, and then echoes the name of each individual property (each performance counter) and its counter type. And just to be nice, the script echoes the name of the counter type (such as PERF_100NSEC_TIMER_INV) rather than the decimal value, making it much easier for you to look up the relevant information.

strComputer = "."
strNamespace = "root\cimv2"
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
    
For Each objclass2 in objWMIService.SubclassesOf()
    If Left(objClass2.Path_.Class,13) = "Win32_PerfRaw" Then
        strClass = objClass2.Path_.Class     
        Set objClass = GetObject("winmgmts:\\" & strComputer & _
            "\" & strNameSpace & ":" & strClass)
        For Each objClassProperty In objClass.Properties_
            strType = ""
            strFormula = ""
            For Each objClassQualifier In objClassProperty.Qualifiers_
                If objClassQualifier.Name = "countertype" Then
                Select Case objClassQualifier.Value
                    Case 0 
                        strType = "PERF_COUNTER_RAWCOUNT_HEX"
                    Case 1073874176
                        strType = "PERF_AVERAGE_BULK"
                    Case 1073939457
                        strType = "PERF_SAMPLE_BASE"
                    Case 1073939458
                        strType = "PERF_AVERAGE_BASE"
                    Case 1073939459
                        strType = "PERF_RAW_BASE"
                    Case 1073939712
                        strType = "PERF_LARGE_RAW_BASE"
                    Case 1107494144
                        strType = "PER_COUNTER_MULTI_BASE"
                    Case 256
                        strType = "PERF_COUNTER_LARGE_RAWCOUNT_HEX"
                    Case 272696320
                        strType = "PERF_COUNTER_COUNTER"
                    Case 272696576
                        strType = "PERF_COUNTER_BULK_COUNT"
                    Case 2816
                        strType = "PERF_COUNTER_TEXT"
                    Case 591463680
                        strType = "PERF_COUNTER_MULTI_TIMER_INV"
                    Case 4195238
                        strType = "PERF_COUNTER_DELTA"
                    Case 4195584
                        strType = "PERF_COUNTER_LARGE_DELTA"
                    Case 4260864
                        strType = "PERF_SAMPLE_COUNTER"
                    Case 4523008
                        strType = "PERF_COUNTER_QUELEN_TYPE"
                    Case 537003008
                        strType = "PERF_RAW_FRACTION"
                    Case 541525248
                        strType = "PERF_PRECISION_SYSTEM_TIMER"
                    Case 558957824
                        strType = "PERF_100NSEC_TIMER_INV"
                    Case 542180608
                        strType = "PERF_100NSEC_TIMER"
                    Case 542573824
                        strType = "PERF_PRECISION_100NS_TIMER"
                    Case 543229184
                        strType = "PERF_OBJ_TIME_TIMER"
                    Case 549585920
                        strType = "PERF_SAMPLE_FRACTION"
                    Case 4523264
                        strType = "PERF_COUNTER_LARGE_QUELEN_TYPE"
                    Case 5571840
                        strType = "PERF_COUNTER_100NS_QUELEN_TYPE"
                    Case 541132032
                        strType = "PERF_COUNTER_TIMER"
                    Case 574686464
                        strType = "PERF_COUNTER_MULTI_TIMER"
                    Case 575735040
                        strType = "PERF_100NSEC_MULTI_TIMER"
                    Case 592512256
                        strType = "PERF_100NSEC_MULTI_TIMER_INV"
                    Case 65536
                        strType = "PERF_COUNTER_RAWCOUNT"
                    Case 65792
                        strType = "PERF_COUNTER_LARGE_RAWCOUNT"
                    Case 6620416
                        strType = "PERF_COUNTER_OBJ_TIME_QUELEN_TYPE"
                    Case 805438464
                        strType = "PERF_AVERAGE_TIMER"
                    Case 807666944
                        strType = "PERF_ELAPSED_TIME"
                    Case 557909248
                        strType = "PERF_COUNTER_TIMER_INV"
                    Case Else
                        strType = "Counter type could not be determined."
                    End Select
                End If
            Next
            WScript.Echo strClass & "." & objClassProperty.Name & "," & strType
        Next
    End If
Next

Of course, it would really be nice to have a script that not only echoed the counter type, but also echoed the formula, the property used to determine the time base, and maybe even showed how you could use the counter type in a script. Well, such a script would be a bit too long for this column, but have no fear. Instead, keep your eyes on the TechNet Script Center, and watch for the Performomatic 2000. That should do everything you could ever want in such a script ... and maybe even a little bit more.

Have questions or comments about either this column or about the possibility of creating a superhuman super being? Send them to scripter@microsoft.com.

 

Scripting Clinic

Greg Stemp has long been acknowledged as one of the country's foremost authorities on scripting, and has been widely acclaimed as a world-class... huh? Well, how come they let football coaches make up stuff on their resumes? Really? He got fired? Oh, all right. Greg Stemp works at... Oh, come on now, can't I even say that? Fine. Greg Stemp gets paid by Microsoft, where he tenuously holds the title of lead writer for the System Administration Scripting Guide.