Public/Get-ProcessByCPU.ps1
|
function Get-ProcessByCPU { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [string[]]$ComputerName, [Parameter(Mandatory = $false, Position = 1)] [string]$ExportCsvPath, [Parameter(Mandatory = $false)] [string]$ProcessName, [Parameter(Mandatory = $false)] [int]$Top = 5, [Parameter(Mandatory = $false)] [int]$MaxThreads = 10 ) begin { $allComputers = [System.Collections.Generic.List[string]]::new() } process { foreach ($computer in $ComputerName) { $allComputers.Add($computer) } } end { Write-Verbose "Initializing Multi-Threaded Runspace Pool with a maximum of $MaxThreads threads..." # 1. Define the code that each thread will execute $scriptBlock = { param($targetComputer, $procFilterName, $topCount) try { $sessionParams = @{ ComputerName = $targetComputer; ErrorAction = 'Stop' } # 1a. Gather server-side filtered performance metrics $filter = "Name != '_Total' AND Name != 'Idle' AND PercentProcessorTime > 0" if ($procFilterName) { $sanitizedName = $procFilterName -replace "'", "''" $filter += " AND Name LIKE '%$sanitizedName%'" } $perfInstances = Get-CimInstance -ClassName 'Win32_PerfFormattedData_PerfProc_Process' -Filter $filter @sessionParams if (-not $perfInstances) { return } # 1b. Query all processes at once to avoid WQL string length limits $processDetails = Get-CimInstance -ClassName 'Win32_Process' @sessionParams # 1c. Build a lookup table containing both Owner and Memory metrics $processLookup = @{} foreach ($proc in $processDetails) { $ownerInfo = Invoke-CimMethod -InputObject $proc -MethodName GetOwner -ErrorAction SilentlyContinue $memoryMb = if ($proc.WorkingSetSize) { [math]::Round($proc.WorkingSetSize / 1MB, 2) } else { 0 } $ownerName = if ($ownerInfo.ReturnValue -eq 0) { "$($ownerInfo.Domain)\$($ownerInfo.User)" } else { "N/A" } $processLookup[$proc.ProcessId] = @{ Owner = $ownerName Memory = $memoryMb } } # 1d. Synthesize and merge the performance data $results = foreach ($instance in $perfInstances) { $processId = $instance.IDProcess $extraData = $processLookup[$processId] [pscustomobject]@{ PSComputerName = $targetComputer Name = $instance.Name PercentProcessorTime = $instance.PercentProcessorTime Memory_MB = if ($extraData) { $extraData.Memory } else { 0 } IDProcess = $processId OwnerID = if ($extraData) { $extraData.Owner } else { "N/A" } } } # Sort and return the requested top subset per machine $results | Sort-Object -Property PercentProcessorTime -Descending | Select-Object -First $topCount } catch { Write-Error "Thread failed for $targetComputer. Error: $_" } } # 2. Setup the Runspace Pool $runspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $runspacePool.Open() # 3. Create tracking objects for our running threads $threads = [System.Collections.Generic.List[pscustomobject]]::new() $finalOutput = [System.Collections.Generic.List[pscustomobject]]::new() # 4. Launch a thread for every computer in parallel foreach ($computer in $allComputers) { $psInstance = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($ProcessName).AddArgument($Top) $psInstance.RunspacePool = $runspacePool # BeginInvoke starts the thread asynchronously $handle = $psInstance.BeginInvoke() $threads.Add([pscustomobject]@{ Instance = $psInstance Handle = $handle Computer = $computer }) } Write-Verbose "All threads spun up. Collecting data as they complete..." # 5. Monitor and collect results from the threads while ($threads.Count -gt 0) { $completedThreads = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($thread in $threads) { if ($thread.Handle.IsCompleted) { # Extract the collected custom objects from the thread $data = $thread.Instance.EndInvoke($thread.Handle) if ($data) { $finalOutput.AddRange($data) } # Clean up the system memory for this thread $thread.Instance.Dispose() $completedThreads.Add($thread) } } # Remove completed tasks from active tracking loop foreach ($completed in $completedThreads) { [void]$threads.Remove($completed) } # Tiny pause to prevent CPU spiking while waiting Start-Sleep -Milliseconds 100 } # 6. Tear down the pool safely $runspacePool.Close() $runspacePool.Dispose() # 7. Output processing logic if ($PSBoundParameters.ContainsKey('ExportCsvPath') -and $finalOutput.Count -gt 0) { $finalOutput | Export-Csv -Path $ExportCsvPath -NoTypeInformation -Encoding utf8 -Append Write-Verbose "Thread results successfully merged and exported to $ExportCsvPath" } else { $finalOutput } } } |