Accessing Performance Counters in PowerShell

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))
        {
            $notDisplay
ed = ($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]

9 Responses to “Accessing Performance Counters in PowerShell”

  1. /\/\o\/\/ writes:

    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\/\/

  2. Lee writes:

    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

  3. /\/\o\/\/ writes:

    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

  4. Hugo writes:

    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

  5. Lee writes:

    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

  6. Johnny Wang writes:

    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!

  7. Roy writes:

    @ 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.

  8. lx writes:

    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.

  9. Pan writes:

    Nice looking script, but it show me zero CPU for all processes. I still get zero and with MWI Win32_PerfFormattedData_PerfProc_Process, and I search from weeks for at least 1 method that work and give to me the process CPU%, damn…

Leave a Reply