Accessing Performance Counters in PowerShell
Friday, 9 December 2005
The question came up on the newsgroup on how to monitor CPU usage in PowerShell. I wrote a script to demonstrate this some time ago with the intent to write about it – so now is probably an ideal time.
The following poll-process script retrieves the process name, main window title, processor usage, disk activity, and working set. It continually refreshes the display to give you a task manager-like experience.
One advantage it offers over Task Manager is the disk activity column. Since total disk activity is actually measured by several separate perf counters, it uses a heuristic to combine them into a single number.
Now, when you hear something drilling holes in your hard drive, you can find out what it is.
## poll-process.ps1
## Continuously display a process list, sorted
## by the desired criteria.
##
## usage: poll-process [sortCriteria] [pollInterval]
##
## sortCriteria must be one of ”Id”, ”ProcessName”, ”MainWindowTitle”,
## ”Processor”, ”Disk”, or ”WorkingSet”
## pollInterval is specified in milliseconds
##
param(
[string] $sortCriteria = “Processor”,
[int] $pollInterval = 750
)
function main
{
## Store the performance counters we need
## for the CPU, and Disk I/O numbers
$cpuPerfCounters = @{}
$ioOtherOpsPerfCounters = @{}
$ioOtherBytesPerfCounters = @{}
$ioDataOpsPerfCounters = @{}
$ioDataBytesPerfCounters = @{}
$processes = $null
$lastPoll = get-date
$lastSnapshotCount = 0
$lastWindowHeight = 0
## The coordinates to which we position the output
$coords = new-object System.Management.Automation.Host.Coordinates 0,0
clear-host
while(-not $host.UI.RawUI.KeyAvailable)
{
## Set the cursor position, get the processes, and store
## the time of the snapshot
$host.UI.RawUI.CursorPosition = $coords
$processes = get-process | sort Id
## Go through all of the processes we captured
foreach($process in $processes)
{
## Get the disk activity, based on I/O Perf Counters,
## for the process in question. Then, add it as a note.
$activity = get-diskActivity $process
$process | add-member NoteProperty Disk $activity
$cpuPercent = get-cpuPercent $process
$process | add-member NoteProperty Processor $cpuPercent
}
$windowHeight = $host.Ui.RawUi.WindowSize.Height
## Since clear-host makes the screen flash, we only do so when
## we have fewer processes than we did the last time
if(($processes.Count -lt $lastSnapshotCount) -or `
(-not ($lastWindowHeight -eq $windowHeight)))
{
clear-host
}
## Tailor the length of the list to the height of the
## window. If the window is to short, show no output.
if($windowHeight -le 7)
{
$output = $null
}
else
{
$output = $processes | sort -desc $sortCriteria | `
select-object -First ($windowHeight - 7)
}
## Display the results
$output | format-table Id,ProcessName,MainWindowTitle,Processor,Disk,WorkingSet
if($processes.Count -gt ($windowHeight - 7))
{
$notDisplayed = ($processes.Count - $output.Count)
“[ $notDisplayed process(es) not shown ]“
}
$lastSnapshotCount = $processes.Count
$lastWindowHeight = $windowHeight
## Sleep for their desired amount of elapsed time,
## adjusted for how much time we’ve actually spent working.
$elapsed = [int] (get-date).Subtract($lastPoll).TotalMilliseconds
$lastPoll = get-date
if($pollInterval -gt $elapsed)
{
start-sleep -m ($pollInterval - $elapsed)
}
}
}
## As a heuristic, gets the total IO and Data operations per second, and
## returns their sum.
function get-diskActivity (
$process = $(throw “Please specify a process for which to get disk usage.”)
)
{
$processName = get-processName $process
## We store the performance counter objects in a hashtable. If we don’t,
## then they fail to return any information for a few seconds.
if(-not $ioOtherOpsPerfCounters[$processName])
{
$ioOtherOpsPerfCounters[$processName] = `
new-object System.Diagnostics.PerformanceCounter `
“Process”,“IO Other Operations/sec”,$processName
}
if(-not $ioOtherBytesPerfCounters[$processName])
{
$ioOtherBytesPerfCounters[$processName] = `
new-object System.Diagnostics.PerformanceCounter `
“Process”,“IO Other Bytes/sec”,$processName
}
if(-not $ioDataOpsPerfCounters[$processName])
{
$ioDataOpsPerfCounters[$processName] = `
new-object System.Diagnostics.PerformanceCounter `
“Process”,“IO Data Operations/sec”,$processName
}
if(-not $ioDataBytesPerfCounters[$processName])
{
$ioDataBytesPerfCounters[$processName] = `
new-object System.Diagnostics.PerformanceCounter `
“Process”,“IO Data Bytes/sec”,$processName
}
## If a process exits between the time we capture the processes and now,
## then we will be unable to get its NextValue(). This trap simply
## catches the error and continues.
trap { continue; }
## Get the performance counter values
$ioOther = (100 * $ioOtherOpsPerfCounters[$processName].NextValue()) + `
($ioOtherBytesPerfCounters[$processName].NextValue())
$ioData = (100 * $ioDataOpsPerfCounters[$processName].NextValue()) + `
($ioDataBytesPerfCounters[$processName].NextValue())
return [int] ($ioOther + $ioData)
}
## Get the percentage of time spent by a process.
## Note: this is multiproc ”unaware.” We need to divide the
## result by the number of processors.
function get-cpuPercent (
$process = $(throw “Please specify a process for which to get CPU usage.”)
)
{
$processName = get-processName $process
## We store the performance counter objects in a hashtable. If we don’t,
## then they fail to return any information for a few seconds.
if(-not $cpuPerfCounters[$processName])
{
$cpuPerfCounters[$processName] = `
new-object System.Diagnostics.PerformanceCounter `
“Process”,“% Processor Time”,$processName
}
## If a process exits between the time we capture the processes and now,
## then we will be unable to get its NextValue(). This trap simply
## catches the error and continues.
trap { continue; }
## Get the performance counter values
$cpuTime = ($cpuPerfCounters[$processName].NextValue() / $env:NUMBER_OF_PROCESSORS)
return [int] $cpuTime
}
## Performance counters are keyed by process name. However,
## processes may share the same name, so duplicates are named
## <process>#1, <process>#2, etc.
function get-processName (
$process = $(throw “Please specify a process for which to get the name.”)
)
{
## If a process exits between the time we capture the processes and now,
## then we will be unable to get its information. This simply
## ignores the error.
$errorActionPreference = “SilentlyContinue”
$processName = $process.ProcessName
$localProcesses = get-process -ProcessName $processName | sort Id
if(@($localProcesses).Count -gt 1)
{
## Determine where this one sits in the list
$processNumber = -1
for($counter = 0; $counter -lt $localProcesses.Count; $counter++)
{
if($localProcesses[$counter].Id -eq $process.Id) { break }
}
## Append its unique identifier, if required
$processName += “#$counter”
}
return $processName
}
. main
[Update: MOW wrote a piece about Perf Counters as well, based on the same newsgroup thread: Getting Performance Monitor Info from Monad]
[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]
[Edit: Script updated for PowerShell RC2]

Subscribe to this blog.
No. 1 — December 9th, 2005 at 9:14 pm
Very Cool,
but if the list of processes is longer then the list, this is done before they are sorted, so if sorted 0n processor you may miss some items in the top of the list.
gr /\/\o\/\/
No. 2 — December 9th, 2005 at 9:24 pm
Actually, in this case, the script sorts before selecting the top entries:
$output = $processes | sort -desc $sortCriteria | `
select-object -First ($windowHeight – 7)
Thanks for the comment, though!
Lee
No. 3 — December 10th, 2005 at 2:04 am
Oops, sorry i did not look well enough,
but still I miss half of the processes i have in the top list of taskmanager.
but in msh counter is 0, hence they are lower in the list maybe it’s the multithreading, if I sample with WMI I get also different readings.
thx for the update ;-)
gr /\/\o\/\/
(this process (IE) is 25 % in Task Manager and 0 % in poll-process.msh
MSH>(get-wmiobject Win32_PerfFormattedData_PerfProc_Process -filter "IDProcess = 4312").PercentProcessorTime
100
50
100
100
0
100
100
100
0
100
100
No. 4 — November 17th, 2006 at 1:49 pm
The script uses 90% of the cpu in executing, I like the script but it should not use more than 5% of cpu for reporting statistics, it it alters so much the system what it is reporting is wrong. Maybe there is something that can be done to reduce this problem.
Keep up the good work!
Hugo
No. 5 — November 17th, 2006 at 8:54 pm
Yes, the performance on this is a hog. The purpose of this was really to show how to access performance counters, so I haven’t looked into why.
Lee
No. 6 — September 7th, 2009 at 3:35 pm
this is horrific!
If I have to do this much work and still not get %CPU then the world is really screwed!
Maybe somone can explain me why get-process doesn’t have the MOST COMMONLY USED column aka CPU USAGE … something which EVERY admin in the world looks for when performance is a problem on any machine!
How on earth could microsoft forget to put that in the freaking get-process command? CPU seconds? Who gives a cr*p about CPU seconds! I want CPU% as it is happening now!
I’m just shaking my head in disbelief!
No. 7 — April 5th, 2011 at 5:03 am
@ Johnny Wang, Oh come-on cut Micro$oft some slack. This is their second administration language which doesn’t require programmer level ability. [Their first was batch.] It only took them 15ish years to figure out that administrators needed more then just batch. And if they made Powershell too useful on the first version why would any need to install the next version…. …or much like everything else they rush to release without closing obvious bugs / deficiencies.
No. 8 — June 3rd, 2011 at 4:17 am
Hhh… I did hope that I was blind or stupid not being able to see the CPU% metrics in the output of get-process – so I googled, so I found this page…
M$ slowly, little step by little step smuggled the features of unix into win, features that made unix lovable – and it was really the way to go. One evidence is myself, the fact that I am willing to touch windows which I wouldn’t have thought a couple of years ago.
Now, it is high time to make those in the marketing department who keep saying bullshit to the developers shut up after all and carve things so they be reasonable and not just show bigger (I know, they got used to create large numbers for versions, because the bigger is the better) senseless numbers.