Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

There’s Sum-thing Happening Here

Posted on February 24, 2014

calculatorI am one of those IT Pros who keeps close tabs on system resources. I like to know what is being used and by what. As you might imagine, a cmdlet like Get-Process, is pretty useful to me. One of the things I'm always checking is how much memory Google Chrome is taking. I don't mean to pick on Chrome as I derive great benefit from it. But because I keep it open for days at a time I think system resources get a little carried away. So every once in a while I like to see how many Chrome processes are running and how much they are using.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!
PS C:\> get-process chrome

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                   
-------  ------    -----      ----- -----   ------     -- -----------                              
    177      26    35564      35104   203     1.61   1976 chrome                                   
    197      45    98676     107984   263    57.55   3016 chrome                                   
    179      25    26872      20500   187     0.36   5956 chrome                                   
    261      33    78456     103908   413   165.72   6260 chrome                                   
    187      26    39656      39980   198    21.00   7744 chrome                                   
    180      32    88896      85836   263    58.20   7828 chrome                                   
   1610      92   119076     178436   492   286.77   8276 chrome                                   
    192      38    87136      91124   257    13.98   9140 chrome                                   
    193      39    73280      82288   245   122.19   9284 chrome                                   
    194      27    57264      50276   204    15.20   9932 chrome                                   
    201      32    22936      26780   264    34.91  10076 chrome

Now, what I really want is a total. I can get that for a single property easy enough using Measure-Object.

PS C:\> get-process chrome | measure vm -sum

Count    : 11
Average  :
Sum      : 3151364096
Maximum  :
Minimum  :
Property : VM

PS C:\> (get-process chrome | measure vm -sum).sum/1mb
3000.12890625

But ideally I'd like to get totals for all of the properties I see with Get-Process. So I wrote an advanced PowerShell function called Get-ProcessTotal.

#requires -version 3.0


Function Get-ProcessTotal {

<#
.SYNOPSIS
Get total values for multiple instances of same process

.DESCRIPTION
This command is designed to get the total value of common process properties:
Handles,NPM,PM,WS,VM, and CPU. The default behavior is to get the sum total
of these properties for processes that might have multiple instances such as
svchost. Although you can also use a wildcard for the process name and get 
totals for a group of processes.  As an alternative to the sum, you can get the
average of these properties.

The command writes a custom object to the pipeline and uses a custom format view
so that the output looks like what you get with Get-Process. This command will 
create a format ps1xml file on-the-fly called ProcessTotal.Format.ps1xml and
store it in the windowsPowerShell folder under Documents. After the file has been
created this command will simply load it into your PowerShell session.

The command writes a custom object that will include the total number of 
processes as well as a property that contains the original processes.

.PARAMETER Name
The name of the process you want to measure. You can use a wildcard. See 
the examples.

.EXAMPLE
PS C:\> get-processtotal chrome

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s) ProcessName                                   
-------  ------    -----      ----- -----   ------ -----------                                   
   4801     612  1120888    1282976  4444  2153.59 chrome    

.EXAMPLE
PS C:\> get-processtotal chrome -average | select *


Handles      : 300
NPM          : 39135
PM           : 71778048
WS           : 82160640
VM           : 291475200
CPU          : 134.6279296875
ProcessName  : chrome
Computername : JH-WIN81-ENT
Total        : 16
Processes    : {System.Diagnostics.Process (chrome), System.Diagnostics.Process (chrome), 
               System.Diagnostics.Process (chrome), System.Diagnostics.Process (chrome)...}
Measure      : Average

.EXAMPLE
PS C:\> get-processtotal vm* -comp chi-hvr2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s) ProcessName                                   
-------  ------    -----      ----- -----   ------ -----------                                   
   3193     205   110904     166300   873        0 vm*     

CPU values are not available for remote computers.

.EXAMPLE
PS C:\> invoke-command {mkdir $env:userprofile\Documents\WindowsPowerShell | out-null} -comp chi-hvr2

PS C:\> invoke-command $(get-item Function:\Get-ProcessTotal).Scriptblock -comp chi-hvr2 -arg VM*

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s) ProcessName             PSComputerName        
-------  ------    -----      ----- -----   ------ -----------             --------------        
   3198     206   111012     166352   874 10178.39 VM*                     chi-hvr2

The first command creates the WindowsPowerShell folder on the remote computer.
The second command invokes the Get-ProcessTotal command on the remote computer
which creates the format ps1xml file also remotely. 

.NOTES
Last Updated: 2/21/2014
Version     : 0.9

Learn more:
 PowerShell in Depth: An Administrator's Guide
 PowerShell Deep Dives 
 Learn PowerShell 3 in a Month of Lunches 
 Learn PowerShell Toolmaking in a Month of Lunches 
 
   ****************************************************************
   * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
   * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
   * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
   * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
   ****************************************************************

.LINK
https://jdhitsolutions.com/blog/2014/02/theres-sum-thing-happening-here

.LINK
Get-Process
Measure-Object

.INPUTS
String

.OUTPUTS
Custom object
#>


[cmdletbinding(DefaultParameterSetName="Sum")]
Param(
[Parameter(Position=0, Mandatory=$True,
HelpMessage = "Enter the name of a process")]
[ValidateNotNullorEmpty()]
[Alias("processname")]
[string]$Name,

[Parameter(ParameterSetName="Sum")]
[switch]$Sum,

[Parameter(ParameterSetName="Average")]
[switch]$Average,

[ValidateNotNullorEmpty()]
[string]$Computername=$env:COMPUTERNAME
)

Begin {
    Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
    Write-Verbose -Message "Querying processes on $computername"
    if ($Average) {
        $measure="Average"
    }
    else {
        $measure="Sum"
    }
    Write-Verbose -Message "Measuring the $measure"
    #update format data if not found
    if (-Not (Get-FormatData -TypeName "My.ProcessTotal")) {
      
      $formatXML=Join-Path -path "$env:USERPROFILE\Documents\WindowsPowerShell" -ChildPath "ProcessTotal.format.ps1xml"
      
      #test if ps1xml file exists and if not create the file
      if (-Not (Test-Path -path $formatXMl)) {
      #the format xml. The Here string must be left justified to start and finish
      $xml=@'
<?xml version="1.0" encoding="utf-8" ?>
<!-- *******************************************************************
This was copied from the DotNetType.format.ps1xml file and modified
to fit my needs.
******************************************************************** -->
<Configuration>
    <ViewDefinitions>
      <View>
            <Name>process</Name>
            <ViewSelectedBy>
                <TypeName>My.ProcessTotal</TypeName>
            </ViewSelectedBy>
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Label>Handles</Label>
                        <Width>7</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>NPM(K)</Label>
                        <Width>7</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>PM(K)</Label>
                        <Width>8</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>WS(K)</Label>
                        <Width>10</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>VM(M)</Label>
                        <Width>5</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>CPU(s)</Label>
                        <Width>8</Width>
                        <Alignment>right</Alignment>
                    </TableColumnHeader>
                    <TableColumnHeader />
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <ScriptBlock>[int]$_.Handles</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>[int]($_.NPM / 1024)</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>[int]($_.PM / 1024)</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>[int]($_.WS / 1024)</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>[int]($_.VM / 1048576)</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>
if ($_.CPU -ne $()) 
{ 
   [math]::Round($_.CPU,2)
}
else {
   "NA"
}
				</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>ProcessName</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                </TableRowEntries>
            </TableControl>
        </View>
    </ViewDefinitions>
</Configuration>
'@
        Try {
            $xml | Out-File -FilePath $formatXML -Encoding ascii -ErrorAction Stop
           
        }
        Catch {
            Write-Warning "Failed to create format xml file $formatXML"
            Write-Warning $_.Exception.Message
        } 
      } #if not test path for format xml file

       #update format data
       Write-Verbose -Message "Adding a custom format view from $formatxml."
       Update-FormatData -AppendPath $formatXML -ErrorAction Stop
    } #if not get-formatdata

    #define a hashtable of parameters to splat to Measure-Object so we 
    #can get either -Sum or -Average
    $measureParams=@{$Measure=$True;Property=$Null}
} #begin

Process {

Try {
    Write-Verbose -Message "Getting processes for $Name"
    $processes = Get-Process $Name -Computername $computername -ErrorAction Stop
}
Catch {
    Write-Warning "Failed to find process $name on $computername"
    #bail out
    Return
}

If ($processes) {
    Write-Verbose -message "Defining a hash table of property values"
    $properties="Handles","NPM","PM","WS","VM","CPU"
    #initialize the ordered hash table
    $propertyHash=[ordered]@{}
    #add each property to the hash table getting the sum or average
    foreach ($property in $properties) {
       Write-Verbose -message "Adding $property"
       $measureParams.Property = $property
       $value = $processes | Measure-Object @measureParams | Select -ExpandProperty $measure
       $propertyHash.Add($property,$value)
    } #foreach property

    Write-Verbose -message "Adding remaining properties"
    $propertyHash.Add("ProcessName",$Name)
    $propertyHash.Add("Computername",$processes[0].MachineName)
    $propertyHash.Add("Total",$processes.count)
    $propertyHash.Add("Processes",$processes)
    $propertyHash.Add("Measure",$Measure)

    #create a custom object
    Write-Verbose -Message "Creating object from hashtable"
    $obj = New-Object -TypeName psobject -Property $propertyHash 

    #add a type name
    Write-Verbose -Message "Adding type name"
    $obj.psobject.TypeNames[0]="My.ProcessTotal"

    #write the object to the pipeline
    $obj

} #if $processes
} #process

End {
    Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end
} #end function

#add an optional alias
Set-Alias -Name gpt -Value Get-ProcessTotal

The main part of the function gets all instances of a process and then creates a custom object with a sum total for several properties.

$properties="Handles","NPM","PM","WS","VM","CPU"
    #initialize the ordered hash table
    $propertyHash=[ordered]@{}
    #add each property to the hash table getting the sum or average
    foreach ($property in $properties) {
       Write-Verbose -message "Adding $property"
       $measureParams.Property = $property
       $value = $processes | Measure-Object @measureParams | Select -ExpandProperty $measure
       $propertyHash.Add($property,$value)
    } #foreach property

The output also includes the computer name, in case you are querying a remote computer, the total count and also the original process objects in case you need to do something else with them.

$propertyHash.Add("ProcessName",$Name)
    $propertyHash.Add("Computername",$processes[0].MachineName)
    $propertyHash.Add("Total",$processes.count)
    $propertyHash.Add("Processes",$processes)
    $propertyHash.Add("Measure",$Measure)

    #create a custom object
    Write-Verbose -Message "Creating object from hashtable"
    $obj = New-Object -TypeName psobject -Property $propertyHash

That could be sufficient, but all the values would be in bytes and the default display would be a list. Instead I wanted default output like I get with Get-Process. So I took advantage of PowerShell's extensible type system and created my own format data xml file. Actually what I did was to find the section in the DotNetTypes.format.ps1xml file for process objects, copied it and tweaked it.

My original idea was to have a separate XML file. If I was building a module, that would be the best choice. But this was a single, stand-alone function so I added some logic to create the XML file on first use and add it to my PowerShell session. The XML content is stored as a Here string in the function.

if (-Not (Get-FormatData -TypeName "My.ProcessTotal")) {
      
      $formatXML=Join-Path -path "$env:USERPROFILE\Documents\WindowsPowerShell" -ChildPath "ProcessTotal.format.ps1xml"
      
      #test if ps1xml file exists and if not create the file
      if (-Not (Test-Path -path $formatXMl)) {
      #the format xml. The Here string must be left justified to start and finish
      $xml=@'
<?xml version="1.0" encoding="utf-8" ?>
...
Try {
            $xml | Out-File -FilePath $formatXML -Encoding ascii -ErrorAction Stop
           
        }
        Catch {
            Write-Warning "Failed to create format xml file $formatXML"
            Write-Warning $_.Exception.Message
        } 
      } #if not test path for format xml file

       #update format data
       Write-Verbose -Message "Adding a custom format view from $formatxml."
       Update-FormatData -AppendPath $formatXML -ErrorAction Stop
    } #if not get-formatdata

In short, the function first tests to see if there is format data for an object of type My.ProcessTotal. If not, in then tests for the XML file which I'm storing in the WindowsPowerShell folder. If it doesn't exist, the file is created. In any event once the file exists it is then loaded into PowerShell using Update-FormatData.

How does PowerShell know what type to use? I told it. When I defined my custom object I also gave it a new type name.

$obj.psobject.TypeNames[0]="My.ProcessTotal"

Once the function is loaded into my session I can now get a result like this:

PS C:\> Get-ProcessTotal chrome

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s) ProcessName
-------  ------    -----      ----- -----   ------ -----------
   3560     424   747336     855356  3027   948.83 chrome

The output is just like Get-Process except the values are sums. I also added an option to get an average instead just in case. If you pipe to Get-Member you'll see the new type definition, as well as the other non-default properties.

PS C:\> Get-ProcessTotal chrome | get-member


   TypeName: My.ProcessTotal

Name         MemberType   Definition
----         ----------   ----------
Equals       Method       bool Equals(System.Object obj)
GetHashCode  Method       int GetHashCode()
GetType      Method       type GetType()
ToString     Method       string ToString()
Computername NoteProperty System.String Computername=JH-WIN81-ENT
CPU          NoteProperty System.Double CPU=960.484375
Handles      NoteProperty System.Double Handles=3569
Measure      NoteProperty System.String Measure=Sum
NPM          NoteProperty System.Double NPM=435088
PM           NoteProperty System.Double PM=762679296
Processes    NoteProperty System.Object[] Processes=System.Object[]
ProcessName  NoteProperty System.String ProcessName=chrome
Total        NoteProperty System.Int32 Total=11
VM           NoteProperty System.Double VM=3170050048
WS           NoteProperty System.Double WS=879349760

There are some other examples in the comment based help. The script also creates an alias so if you don't want it be sure to comment out the line at the end.

I hope some of you will kick it around and let me know what you think. Or at the very least I hope you picked up a new technique or idea.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d