Private/Kinds/Memory.ps1
|
# The Memory Kind: how much of the memory is in use, and what uses it. # # Get-MemoryData talks to the Target Machine and decides nothing. # ConvertTo-MemoryFinding is pure: data and parameters in, Findings out. Every threshold # lives there as a named parameter, defaulted to the value the Gutcheck script used. # How many readings the Memory Check averages across. The wait between them is shared # with every other Kind - see Private/Sampling.ps1. $script:MemorySampleCount = 3 # What Get-MemoryData hands its Judge. See Private/DataShape.ps1. $script:MemoryDataShape = @{ FreePhysicalMemoryKB = 'Answered' TotalVisibleMemoryKB = 'Answered' PercentCommittedBytesInUse = 'Answered' PagesInputPerSecond = 'List' PageFiles = 'List' TopProcessesByMemory = 'List' TopProgramsByMemory = 'List' # Left out of data from before the Run told its own processes apart. OwnProcesses = 'Optional' } function Get-GutcheckOwnProcessId { <# .SYNOPSIS The processes of this Run: this one, the Elevated Part, and what either started. Part of the Gatherer; reads, and decides nothing. .DESCRIPTION Seen: "pwsh 10,5 GB (2 Prozesse)" first among the programs filling the memory - the Run itself, which a User closes by closing Gutcheck. Never the process the Run was started from: that PowerShell may be the User's own, open all day, and what it holds is the User's. .PARAMETER ElevatedPartProcessId The process of the Elevated Part, as the Run started it; none where it started none. #> [CmdletBinding()] [OutputType([int[]])] param([AllowNull()]$ElevatedPartProcessId) $ids = @($PID) if ($ElevatedPartProcessId) { $ids += [int]$ElevatedPartProcessId } foreach ($parent in @($ids)) { try { foreach ($child in @(Get-CimInstance Win32_Process -Filter "ParentProcessId=$parent" -ErrorAction Stop)) { if ($child.ProcessId) { $ids += [int]$child.ProcessId } } } catch { } } @($ids | Select-Object -Unique) } function Get-MemoryData { <# .SYNOPSIS How much of the memory is in use, and what uses it. Reads, and decides nothing. .PARAMETER Observed What the Run knows: under 'RunProcesses' the process of the Elevated Part it started (ElevatedPartProcessId), which is the Run's own. #> [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{}) # From the counters and not through WMI. See Private/Counter.ps1. $committedPath = '\Memory\% Committed Bytes In Use' $pagesPath = '\Memory\Pages Input/sec' $counter = Open-PerformanceCounter -Path $committedPath, $pagesPath $samples = @(1..$script:MemorySampleCount | ForEach-Object { Write-CheckProgress -Status ((Get-Text 'Console.Progress.Sample') -f $_, $script:MemorySampleCount) ` -Step ($_ - 1) -Of $script:MemorySampleCount Wait-Sample Read-PerformanceCounter -Counter $counter }) Close-PerformanceCounter -Counter $counter $last = $samples | Select-Object -Last 1 $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $pageFiles = @(Get-CimInstance Win32_PageFileUsage -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name = $_.Name AllocatedBaseSizeMB = $_.AllocatedBaseSize PeakUsageMB = $_.PeakUsage } }) $processes = @(Get-Process -ErrorAction SilentlyContinue) # The Run's own processes apart: what Gutcheck holds is not what fills the User's memory. $elevatedPart = Get-DataProperty (Get-ObservedKindData -Observed $Observed -Kind 'RunProcesses') 'ElevatedPartProcessId' $ownIds = @(Get-GutcheckOwnProcessId -ElevatedPartProcessId $elevatedPart) $own = @($processes | Where-Object { $ownIds -contains $_.Id } | ForEach-Object { [pscustomobject]@{ Name = $_.Name Id = $_.Id PrivateMB = [math]::Round($_.PrivateMemorySize64 / 1MB) WorkingSetMB = [math]::Round($_.WorkingSet64 / 1MB) } }) # The same by program: a browser is forty processes of a hundred megabytes each, and # none of them is among the largest processes although the browser is the largest # program. Summed by name, so that "what fills the memory" names what a User can close. $programs = @($processes | Where-Object { $ownIds -notcontains $_.Id } | Group-Object Name | ForEach-Object { [pscustomobject]@{ Name = $_.Name Processes = $_.Count PrivateMB = [math]::Round((($_.Group | Measure-Object PrivateMemorySize64 -Sum).Sum) / 1MB) WorkingSetMB = [math]::Round((($_.Group | Measure-Object WorkingSet64 -Sum).Sum) / 1MB) } } | Sort-Object PrivateMB -Descending | Select-Object -First 12) $top = @($processes | Sort-Object PrivateMemorySize64 -Descending | Select-Object -First 12 | ForEach-Object { [pscustomobject]@{ Name = $_.Name Id = $_.Id PrivateMB = [math]::Round($_.PrivateMemorySize64 / 1MB) WorkingSetMB = [math]::Round($_.WorkingSet64 / 1MB) Handles = $_.HandleCount } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Memory' FreePhysicalMemoryKB = $os.FreePhysicalMemory TotalVisibleMemoryKB = $os.TotalVisibleMemorySize PercentCommittedBytesInUse = $(if ($last) { $last[$committedPath] }) PagesInputPerSecond = @($samples | ForEach-Object { $_[$pagesPath] } | Where-Object { $null -ne $_ }) PageFiles = $pageFiles TopProcessesByMemory = $top TopProgramsByMemory = $programs OwnProcesses = $own } } function Get-MemoryTopProgram { <# .SYNOPSIS The programs that hold the most memory, the largest first. Pure. .DESCRIPTION From what the Gatherer summed by program. Data from before it did has the largest processes only, and those are summed here: less than the truth for a program of many small processes, and still the largest that are known. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [int]$First = 5) $programs = @((Get-DataCollection $Data 'TopProgramsByMemory') | Where-Object { $_ }) if (-not $programs.Count) { # Without the Run's own processes, as the Gatherer sums the programs. $own = @((Get-DataCollection $Data 'OwnProcesses') | ForEach-Object { "$(Get-DataProperty $_ 'Id')" }) $programs = @((Get-DataCollection $Data 'TopProcessesByMemory') | Where-Object { $_ -and $own -notcontains "$(Get-DataProperty $_ 'Id')" } | Group-Object { "$(Get-DataProperty $_ 'Name')" } | ForEach-Object { [pscustomobject]@{ Name = $_.Name Processes = $_.Count PrivateMB = ($_.Group | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'PrivateMB') } | Measure-Object -Sum).Sum } }) } @($programs | Where-Object { "$(Get-DataProperty $_ 'Name')" -and (ConvertTo-Number (Get-DataProperty $_ 'PrivateMB')) -gt 0 } | Sort-Object @{ Expression = { ConvertTo-Number (Get-DataProperty $_ 'PrivateMB') }; Descending = $true } | Select-Object -First $First) } function ConvertTo-MemoryFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-RamInUseFinding -Data $Data -Parameters $Parameters New-CommitChargeFinding -Data $Data -Parameters $Parameters New-PageFaultFinding -Data $Data -Parameters $Parameters New-PageFileFinding -Data $Data -Parameters $Parameters } function ConvertTo-MemorySection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) New-Section -Title (Get-Text 'Title.Memory.TopProcessesByMemory') -Help (Get-Text 'Help.Memory.TopProcessesByMemory') -Row @( (Get-DataCollection $Data 'TopProcessesByMemory') | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Memory.Process')] = Get-DataProperty $_ 'Name' $row[(Get-Text 'Column.Memory.Id')] = Get-DataProperty $_ 'Id' $row[(Get-Text 'Column.Memory.PrivateMB')] = Get-DataProperty $_ 'PrivateMB' $row[(Get-Text 'Column.Memory.WorkingSetMB')] = Get-DataProperty $_ 'WorkingSetMB' $row[(Get-Text 'Column.Memory.Handles')] = Get-DataProperty $_ 'Handles' [pscustomobject]$row } ) } function New-RamInUseFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'RamInUseWarnPercent' 80 $fail = Get-Parameter $Parameters 'RamInUseFailPercent' 90 $free = ConvertTo-Number (Get-DataProperty $Data 'FreePhysicalMemoryKB') $total = ConvertTo-Number (Get-DataProperty $Data 'TotalVisibleMemoryKB') if ($null -eq $free -or $null -eq $total -or $total -le 0) { return New-UnavailableFinding -Category Memory -Check (Get-Text 'Check.Memory.RAMInUse') ` -Hint (Get-Text 'Hint.Memory.TheOperatingSystemDidNot') } # What the Run itself holds in memory is left out: Gutcheck goes when it is done, and # a Run that fills the memory is no User's program to close. By its working set, the # part of it that is in the memory now - what "in use" and "free" are of - and counted # as free beside the percentage, so that the two say the same. $ownMB = (@((Get-DataCollection $Data 'OwnProcesses') | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'WorkingSetMB') } | Where-Object { $null -ne $_ }) | Measure-Object -Sum).Sum if (-not $ownMB) { $ownMB = 0 } $ownKB = [math]::Min($ownMB * 1024, [math]::Max(0, $total - $free)) $usedKB = $total - $free - $ownKB $usedPercent = $usedKB / $total * 100 $severity = Get-Severity $usedPercent $warn $fail $value = @((Get-Text 'Value.Memory.PercentWithFree') -f $usedPercent, (($free + $ownKB) / 1MB)) if ($ownMB -gt 0) { $value += Get-Text 'Value.Memory.OwnLeftOut' ($ownMB / 1024) } # "Full" is no place to start. What fills it is: said with the Finding, where the # memory is short, so that nobody has to go and look for the table first. if ($severity -ne 'OK') { $largest = @(Get-MemoryTopProgram -Data $Data -First ([int](Get-Parameter $Parameters 'TopProgramCount' 5)) | ForEach-Object { $count = [int](ConvertTo-Number (Get-DataProperty $_ 'Processes')) $size = (ConvertTo-Number (Get-DataProperty $_ 'PrivateMB')) / 1024 if ($count -gt 1) { (Get-Text 'Value.Memory.ProgramMany') -f "$(Get-DataProperty $_ 'Name')", $size, $count } else { (Get-Text 'Value.Memory.Program') -f "$(Get-DataProperty $_ 'Name')", $size } }) if ($largest.Count) { $value += (Get-Text 'Value.Memory.Largest') -f ($largest -join '; ') } } New-Finding -Category Memory -Check (Get-Text 'Check.Memory.RAMInUse') -Severity $severity ` -Value ($value -join ' | ') -Meaning (Get-Text 'Meaning.Memory.RamFull') -Hint (Get-Text 'Hint.Memory.RamFull') ` -Reference @(New-Reference -Section 'Title.Memory.TopProcessesByMemory') } function New-CommitChargeFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'CommitWarnPercent' 80 $fail = Get-Parameter $Parameters 'CommitFailPercent' 90 $committed = ConvertTo-Number (Get-DataProperty $Data 'PercentCommittedBytesInUse') if ($null -eq $committed) { return New-UnavailableFinding -Category Memory -Check (Get-Text 'Check.Memory.CommitCharge') ` -Hint (Get-Text 'Hint.Memory.TheMemoryPerformanceCountersDid') } New-Finding -Category Memory -Check (Get-Text 'Check.Memory.CommitCharge') -Severity (Get-Severity $committed $warn $fail) ` -Value ((Get-Text 'Value.Memory.PercentOfLimit') -f $committed) ` -Hint (Get-Text 'Hint.Memory.NearTheCommitLimitPrograms') } function New-PageFaultFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'HardPageFaultsWarnPerSecond' 100 $fail = Get-Parameter $Parameters 'HardPageFaultsFailPerSecond' 300 # Judged as the average across the sample window: one spike is not sustained pressure. $average = Get-SampleAverage (Get-DataProperty $Data 'PagesInputPerSecond') if ($null -eq $average) { return New-UnavailableFinding -Category Memory -Check (Get-Text 'Check.Memory.HardPageFaultsPages') ` -Hint (Get-Text 'Hint.Memory.TheMemoryPerformanceCountersDid2') } New-Finding -Category Memory -Check (Get-Text 'Check.Memory.HardPageFaultsPages') ` -Severity (Get-Severity $average $warn $fail) -Value ('{0:N0}' -f $average) ` -Hint (Get-Text 'Hint.Memory.SustainedHighValuesNotEnough') } function New-PageFileFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $pageFiles = (Get-DataCollection $Data 'PageFiles') if (-not $pageFiles.Count) { return New-Finding -Category Memory -Check (Get-Text 'Check.Memory.PageFile') -Value (Get-Text 'Value.Shared.None') -Severity WARN ` -Hint (Get-Text 'Hint.Memory.NoPageFileProgramsCrash') } $described = $pageFiles | ForEach-Object { Get-Text 'Value.Memory.PageFile' $_.Name $_.AllocatedBaseSizeMB $_.PeakUsageMB } New-Finding -Category Memory -Check (Get-Text 'Check.Memory.PageFile') -Value ($described -join ', ') -Severity INFO } |