PowerShell Problem Solver: Process Performance Reporting
We’re back once again with a problem scenario about getting average processor time, as well as the top five processes that consume the most CPU time. If this is your first time in this series, you’ll definitely want to go back and review the earlier PowerShell Problem Solver articles. The original question I came across wanted to combine processor and process information into a single report, presumably for a group of remote computers. At least that’s the approach I take: if I can do something for one server, I should be able to do it for 10, 100, or 1000 servers.
PowerShell Processor Article Series:
- PowerShell Problem Solver: Processor Loads
- PowerShell Problem Solver: More Processor Performance
- PowerShell Problem Solver: Process CPU Utilization
- PowerShell Problem Solver: Process Performance Counters
- PowerShell Problem Solver: Process Performance Reporting
- PowerShell Problem Solver: Process Performance For All
After everything we looked at over the last several articles, it seems to me that the best way to get the most accurate information is with performance counters. The added benefit is that we can query multiple counters at the same time. Let’s test with a single remote computer.
1 |
$computer = "chi-sql01" |
From the previous articles, I know we will need these counters.
1 |
$counters = "\Process(*)\% Processor Time","\Processor(_Total)\% Processor Time" |
At this point, I just need enough data to help me build the PowerShell commands.
1 |
$data = Get-Counter -Counter $counters -ComputerName $computer -MaxSamples 10 -SampleInterval 2 |
Because I am collecting process data for all processes, this will include items like Idle, System and _Total that I don’t want. So I’ll filter those out with a regular expression and group the results using Group-Object.
1 |
$grouped = ($data.countersamples).where({$_.Path -notmatch "\\\\process\((idle|system|_total)\)"}) | Group –property Path |
So far this is what we have.
To assemble a final object, I’ll need to define some properties. First, let’s get the Processor % Time counter and get the average value.
1 2 3 |
$ProcessorAverage = ($grouped).where({$_.name -match "\\\\processor"}).Group | Measure-Object CookedValue -Average | Select -ExpandProperty Average |
Now let’s get the process counters Group property, which will the collection of all the countersamples, group that collection on their InstanceName property, and then calculate the average CPU time. I’m essentially slinging objects through the pipeline to get my desired result.
1 2 3 |
$ProcessAverages = ($grouped).where({$_.name -notmatch "\\\\processor"}).Group | Group –property InstanceName | Select Name,@{Name="AvgCPUTime";Expression = {[math]::Round(($_.group.cookedvalue | measure-object -average).average,4)}} |
I can now select my top five processes by average CPU time.
1 |
$Top = $ProcessAverages | Sort AvgCPUTime -Descending | Select -first 5 |
Even though I know what the computername is, let’s extract it from the data.
1 |
$computername = $data[0].CounterSamples[0].path.split("\\")[2] |
That should be all the raw data I need.
All that remains is to assemble the pieces into an object that we can write to the pipeline. At this point there are several possibilities.
Let’s create an ordered hashtable from the variables so that the property names will be in the order that I define them.
1 2 3 4 5 |
$hash = [ordered]@{ Computername = $computername.toUpper() Avg%ProcessorTime' = $ProcessorAverage Processes = $Top } |
Now I can create a new object using the hashtable for the properties.
1 |
$obj = New-Object -TypeName PSObject -Property $hash |
The Processes property is a collection of nested objects.
This last step really depends on how you plan to consume the results. For example, perhaps you want to construct the object like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$hash = [ordered]@{ Computername = $computername.toUpper() 'Avg%CPUTime' = $ProcessorAverage } $i=0 foreach ($item in $Top) { #increment the counter $i++ #add each item to the hash table as a separate property $hash.Add("Process_$i",$item) } $obj = New-Object -TypeName PSObject -Property $hash |
In this example, I create a custom property for each process. This could come in handy if I wanted to get the number one process. Although again, the property is a nested object. Or I could make each process into a distinct property.
1 2 3 4 5 6 7 8 9 10 11 |
$hash = [ordered]@{ Computername = $computername.toUpper() 'Avg%CPUTime' = $ProcessorAverage } foreach ($item in $Top) { #add each item to the hash table as a separate property $hash.Add($item.name,$item.AvgCPUTime) } $obj = New-Object -TypeName PSObject -Property $hash |
Personally, I think this approach works fine if you are only querying a single computer. But if you were querying multiple servers, PowerShell would not know how to present the object to you because each object would have a different set of properties. When you are creating PowerShell tools you want to write a single type of object to the pipeline. In this situation, technically that is true. But when it comes time to scale out, this won’t work.
This brings us to the logical conclusion, scaling the process out. But in order for me to properly explain, I’ll need a separate article. Plus, your head might be buzzing a bit with the code samples. I encourage you to try them out. Even if you don’t need the performance counter data, I’m hoping some of the techniques I’ve demonstrated will prove useful.