Arbiter
Arbiter

Reputation: 470

PowerShell property expression increases execution time by 4-5 times

Scroll down for TL;DR

I need to get the following properties for every process as quickly as possible, ideally 5 seconds, maximum 10 seconds: ID, Name, Description, Path, Company, Username, Session ID, StartTime, Memory, CPU (percentage, not time)

To get this data, I put together the following snippet which (I think) is functionally perfect:

$ProcessCPU = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess, PercentProcessorTime
$Processes  = Get-Process -IncludeUserName | 
                Select-Object `
                    @{Name='Id';Expression={[int]$_.Id}}, 
                    @{Name='Name';Expression={[string]$_.Name}}, 
                    @{Name='Description';Expression={[string]$_.Description}}, 
                    @{Name='Path';Expression={[string]$_.Path}}, 
                    @{Name='Company';Expression={[string]$_.Company}}, 
                    @{Name='Username';Expression={[string]$_.UserName}},
                    @{Name='SessionId';Expression={[string]$_.SessionId}}, 
                    @{Name='StartTime';Expression={[string](($_.StartTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))}},  
                    @{Name='MemoryMB';Expression={[int]([math]::Round($_.WorkingSet/1MB,2))}},
                    @{Name='CPUPercent';Expression={
                        [int]($ProcessCPU | ?{'IDProcess' -eq $_.Id}).PercentProcessorTime
                    }}

The issue is that its taking 18-22 seconds to execute, caused by this line (which adds about 16 seconds):

@{Name='CPUPercent';Expression={
     [int]($ProcessCPU | ?{'IDProcess' -eq $_.Id}).PercentProcessorTime
}}
PS C:\Windows\system32> Measure-Command -Expression {
    $ProcessCPU = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess, PercentProcessorTime
    $Processes  = Get-Process -IncludeUserName | 
                    Select-Object `
                        @{Name='Id';Expression={[int]$_.Id}}, 
                        @{Name='Name';Expression={[string]$_.Name}}, 
                        @{Name='Description';Expression={[string]$_.Description}}, 
                        @{Name='Path';Expression={[string]$_.Path}}, 
                        @{Name='Company';Expression={[string]$_.Company}}, 
                        @{Name='Username';Expression={[string]$_.UserName}},
                        @{Name='SessionId';Expression={[string]$_.SessionId}}, 
                        @{Name='StartTime';Expression={[string](($_.StartTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))}},  
                        @{Name='MemoryMB';Expression={[int]([math]::Round($_.WorkingSet/1MB,2))}},
                        @{Name='CPUPercent';Expression={
                            [int]($ProcessCPU | ?{'IDProcess' -eq $_.Id}).PercentProcessorTime
                        }}
}

TotalSeconds      : 19.061206

When I remove the slow property expression noted above and keep the WMI query, execution takes about 4.5 seconds:

Measure-Command -Expression {
    $ProcessCPU = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess, PercentProcessorTime
    $Processes  = Get-Process -IncludeUserName | 
                    Select-Object `
                        @{Name='Id';Expression={[int]$_.Id}}, 
                        @{Name='Name';Expression={[string]$_.Name}}, 
                        @{Name='Description';Expression={[string]$_.Description}}, 
                        @{Name='Path';Expression={[string]$_.Path}}, 
                        @{Name='Company';Expression={[string]$_.Company}}, 
                        @{Name='Username';Expression={[string]$_.UserName}},
                        @{Name='SessionId';Expression={[string]$_.SessionId}}, 
                        @{Name='StartTime';Expression={[string](($_.StartTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))}},  
                        @{Name='MemoryMB';Expression={[int]([math]::Round($_.WorkingSet/1MB,2))}}
}

TotalSeconds      : 4.5202906

I thought that by getting all of the required data in a single query and referring back to the $ProcessCPU array would be fast - but I appreciate I'm iterating through each of the 250 arrays stored in $Processes.

TL;DR:

Is there a more performant method of joining two objects on a common property rather than using iteration as I have above? I.E. $ProcessCPU.IDProcess on $Processes.Id?

I tried the following block to test $Output = $ProcessCPU + $Processes | Group-Object -Property Id, it executed in just 3 seconds, but the output wasn't acceptable:

PS C:\Windows\system32> Measure-Command -Expression {
    $ProcessCPU = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process | Select-Object @{Name='Id';Expression={[int]$_.IDProcess}}, PercentProcessorTime
    $Processes  = Get-Process -IncludeUserName | 
                    Select-Object `
                        @{Name='Id';Expression={[int]$_.Id}}, 
                        @{Name='Name';Expression={[string]$_.Name}}, 
                        @{Name='Description';Expression={[string]$_.Description}}, 
                        @{Name='Path';Expression={[string]$_.Path}}, 
                        @{Name='Company';Expression={[string]$_.Company}}, 
                        @{Name='Username';Expression={[string]$_.UserName}},
                        @{Name='SessionId';Expression={[string]$_.SessionId}}, 
                        @{Name='StartTime';Expression={[string](($_.StartTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))}},  
                        @{Name='MemoryMB';Expression={[int]([math]::Round($_.WorkingSet/1MB,2))}}
    $Output = $ProcessCPU + $Processes | Group-Object -Property Id
}

TotalSeconds      : 2.9656969

enter image description here

Upvotes: 2

Views: 225

Answers (1)

mklement0
mklement0

Reputation: 439193

  • Use CIM to build up a hashtable that maps process IDs (PIDs) to their CPU percentages first.

  • Then make the calculated property passed to Select-Object consult that hashtable for efficient lookups:

Get-CimInstance Win32_PerfFormattedData_PerfProc_Process |
  ForEach-Object -Begin   { $htCpuPctg=@{} } `
                 -Process { $htCpuPctg[$_.IdProcess] = $_.PercentProcessorTime }       #`

Get-Process -IncludeUserName | 
  Select-Object Id,
                Name,
                Description,
                Path,
                Company,
                UserName,
                SessionId,
                @{Name='StartTime';Expression={[string](($_.StartTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))}},  
                @{Name='MemoryMB';Expression={[int]([math]::Round($_.WorkingSet/1MB,2))}},
                @{Name='CPUPercent';Expression={ $htCpuPctg[[uint32] $_.Id] }}

Note:

  • Get-CimInstance rather than Get-WimObject is used, because the CIM cmdlets superseded the WMI cmdlets in PowerShell v3 (released in September 2012). Therefore, the WMI cmdlets should be avoided, not least because PowerShell Core, where all future effort will go, doesn't even have them anymore. For more information, see this answer.

  • There is usually no need to use calculated properties such as @{Name='Id';Expression={[int]$_.Id}} to simply extract a property as-is - just use the property's name - Id - as a Select-Object -Property argument (but you've since clarified that you're using calculated properties because you want explicit control over the property's data type for sending data to an IoT Gateway via JSON).

  • Note that CIM reports PIDs as [uint32]-typed values, whereas Get-Process uses [int] values - hence the need to cast to [uint32] in the hashtable lookup.

Upvotes: 1

Related Questions