PowerShell Cookbook

Search

Categories

 

On this page

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 216
This Year: 16
This Month: 0
This Week: 0
Comments: 523

Sign In

 Friday, December 09, 2005
Friday, December 09, 2005 7:30:45 PM (Pacific Standard Time, UTC-08:00) ( )

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]

Comments [5] | | # 
Friday, December 09, 2005 9:14:30 PM (Pacific Standard Time, UTC-08:00)
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\/\/
Friday, December 09, 2005 9:24:43 PM (Pacific Standard Time, UTC-08:00)
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
Saturday, December 10, 2005 2:04:59 AM (Pacific Standard Time, UTC-08:00)
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
Friday, November 17, 2006 1:49:33 PM (Pacific Standard Time, UTC-08:00)
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
Hugo
Friday, November 17, 2006 8:54:33 PM (Pacific Standard Time, UTC-08:00)
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
Name
E-mail
Home page

Comment (Some html is allowed: b, blockquote@cite, em, i, strike, strong, sub, super, u)  

Enter the code shown (prevents robots):