This week I've been testing out a new browser, Brave, as a possible replacement for Firefox. One of the reasons I switched to Firefox from Chrome was performance and better resource utilization. Brave may now be a better choice, but that's not what this article is about. In order to assess resource utilization I turned to the Get-Process PowerShell cmdlet. Because I found myself needing to get some very specific results, I decided to write a PowerShell function to simplify the process. This is why you should be learning and using PowerShell, to build your own tools to solve your business needs. Here is some of the story on my development process.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Start with the Basics
I first started with a core PowerShell expression to display process information, using the Workingset property.
Get-Process brave | Measure-Object Workingset -sum -average | Select-object Count,Sum,Average
Substitute in any process you want like chrome to see similar results. This simple expression gets all the brave processes and pipes them to Measure-Object which calculates the sum and average of the workingset property. It would be helpful to see the process name in the output especially because I intend to do comparisons with Firefox. If I do this:
Get-Process brave,firefox | Measure-Object Workingset -sum -average | Select-object Count,Sum,Average
I get the sum for both browsers. No. I would need to run Get-Process and Measure-Object for each process. One way would be to use ForEach-Object.
"brave","firefox" | foreach-object { Get-Process -name $_ -PipelineVariable pv | Measure-Object Workingset -sum -average | Select-object @{Name="Name";Expression = {$pv.name}}, Count,Sum,Average }
This is better. I'm using the common PipelineVariable parameter to retrieve the process name. Measure-Object's output doesn't include the process name from Get-Process so in order for Select-Object to work I need a way to access something from earlier in the pipeline. That's what PipelineVariable achieves.
Formatting Values
I don't know about you but I can't readily convert all those bytes into something more meaningful. But PowerShell can.
"brave","firefox" | foreach-object { Get-Process -name $_ -PipelineVariable pv | Measure-Object Workingset -sum -average | Select-object @{Name="Name";Expression = {$pv.name}}, Count, @{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}}, @{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}} }
I now have code that generates the friendly result I need.
But I need to make this flexible and re-usable. This is where functions come into play. I can quickly turn this into a function.
Function Get-MyProcess { [cmdletbinding()] Param([string[]]$Name) $Name | foreach-object { Get-Process -name $_ -PipelineVariable pv | Measure-Object Workingset -sum -average | Select-object @{Name="Name";Expression = {$pv.name}}, Count, @{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}}, @{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}} } }
And it works.
Scaling Out
At this point I could consider this complete. However, when I write PowerShell functions, especially something is getting values or data, I typically want to be able to have it work with remote computers. My initial thought was that because Get-Process has a -Computername parameter, all I had to do was add it to my function and pass it along in the code. The potential downside to this approach is that when you connect to a remote computer in this way, you are connecting over legacy remoting connections. I'm trying to avoid using legacy connections where possible so that means I need to take advantage of PowerShell remoting. One solution would be to wrap my Get-Process command inside an Invoke-Command expression.
Function Get-MyProcess { [cmdletbinding()] Param([string[]]$Name,[string]$Computername = $env:computername) Invoke-Command -ScriptBlock { $using:Name | foreach-object { Get-Process -name $_ -PipelineVariable pv | Measure-Object Workingset -sum -average | Select-object @{Name="Name";Expression = {$pv.name}}, Count, @{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}}, @{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}} } } -ComputerName $computername }
This is getting closer to a mature, PowerShell function.
Functions Don't Format
As you can see, this function doesn't include any error handling. And since I never need to see the RunspaceID property I would always have to wrap my function in another PowerShell expression to remove it. It would be nicer if the function wrote an object to the pipeline with just values I wanted to see. The other potential drawback is that my sum and average values are formatted in MB. I have made an assumption that limits the flexibility of this function. I was initially lazy. The better approach is to write an object to the pipeline with raw, unformatted data.
As I revised the function, this became the core of the scriptblock:
foreach ($item in $ProcessName) { Get-Process -Name $item -PipelineVariable pv -OutVariable ov | Measure-Object -Property WorkingSet -Sum -Average | Select-Object -Property @{Name = "Name"; Expression = {$pv.name}}, Count, @{Name = "Threads"; Expression = {$ov.threads.count}}, Average, Sum, @{Name = "Computername"; Expression = {$env:computername}} }
The output values were now back to bytes. But you know, I really would like to see them formatted as MB by default. PowerShell can do that and if fact does that all the time. When you run Get-Process the default display is formatted for you. What you see is not the raw value of the underlying process objects. PowerShell is using a format directive. You can too.
Format Data
First, you need to give your output object a type name. I could have built a function using a PowerShell class. But it is just as easy to insert a type name.
Invoke-Command @PSBoundParameters | Select-Object -Property * -ExcludeProperty RunspaceID,PS* | ForEach-Object { #insert a custom type name for the format directive $_.psobject.typenames.insert(0, "myProcessMemory") | Out-Null $_ }
Now the tricky part. Format directives are special XML files saved with a .ps1xml file. One of the things I can do is define a custom heading and value.
<TableColumnItem> <ScriptBlock>[math]::Round($_.average/1MB,4)</ScriptBlock> </TableColumnItem>
The default output uses the formatting directives but I can still use the underlying actual property names.
By the way, don't read too much into these results as Firefox has a lot of extensions and Brave has several open tabs.
Instead of creating a separate .ps1xml file, I included the contents as a here string in my script file with my function and create a temporary file which is then imported using Update-FormatData. The complete file is on Github.
Yes, this took a little bit of time to develop, but I now have a re-usable and flexible PowerShell function. and a model that I can use for future functions. I think it is helpful to see the development process as much as the final product. If you have feedback or questions, please feel free to leave a comment. Enjoy!
Update 2 January 2019
I've updated the code in the Github gist to better handle the use of wildcards. I also tweaked the formatting directives.
Great article! I added some info in the Gist for you to review though regarding the use of wildcards that folks might want to avoid using until updated.
Thanks for the comments. Yes, wildcards should be avoided which I tried to stress in the comment based help.