2

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

Arbiter
  • 450
  • 5
  • 26

1 Answers1

1
  • 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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks for the detailed response. I'm using CIM everywhere else, not sure why I even used GWMI here, thanks for the reminder. I need to calculate my properties for reliable typecasting (I'm converting to JSON and submitting it to an IoT Gateway using the MQTT protocol). The last bullet point is great, I never would have noticed. Also, I have measure your answer and it took 26 seconds on the first run and 1.5 seconds for each run after. Do you know why that might be? – Arbiter Nov 14 '19 at 12:32
  • @Arbiter: I would expect there to be some caching effects, but I don't know enough about CIM / WMI to explain the drastic speed-up. – mklement0 Nov 14 '19 at 12:41
  • 1
    It would make sense, I'll just run the command when the service starts to have something cached - I can only hope it remains cached long enough so that the function responds within the 5-second target as and when required. Thank you for your help! – Arbiter Nov 14 '19 at 13:12
  • My pleasure, @Arbiter; fingers crossed. Certainly, the the CPU-percentage data itself by definition _cannot_ be cached if it is to reflect the current conditions. – mklement0 Nov 14 '19 at 13:14