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' } function Get-MemoryData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) # The first read of a formatted-performance class is primed with zeroes; discard it. [void](Get-CimInstance Win32_PerfFormattedData_PerfOS_Memory -ErrorAction SilentlyContinue) $samples = @(1..$script:MemorySampleCount | ForEach-Object { Write-CheckProgress -Status ((Get-Text 'Console.Progress.Sample') -f $_, $script:MemorySampleCount) ` -Step ($_ - 1) -Of $script:MemorySampleCount Wait-Sample Get-CimInstance Win32_PerfFormattedData_PerfOS_Memory -ErrorAction SilentlyContinue }) $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 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 | 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 = $last.PercentCommittedBytesInUse PagesInputPerSecond = @($samples | ForEach-Object { $_.PagesInputPersec }) PageFiles = $pageFiles TopProcessesByMemory = $top TopProgramsByMemory = $programs } } 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) { $programs = @((Get-DataCollection $Data 'TopProcessesByMemory') | Where-Object { $_ } | 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') } $usedPercent = 100 - ($free / $total * 100) $severity = Get-Severity $usedPercent $warn $fail $value = @((Get-Text 'Value.Memory.PercentWithFree') -f $usedPercent, ($free / 1MB)) # "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 { '{0} {1} MB (peak {2} MB)' -f $_.Name, $_.AllocatedBaseSizeMB, $_.PeakUsageMB } New-Finding -Category Memory -Check (Get-Text 'Check.Memory.PageFile') -Value ($described -join ', ') -Severity INFO } |