Private/Kinds/Memory.ps1

# The Memory Kind.
#
# 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

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
        }
    })

    $top = @(Get-Process -ErrorAction SilentlyContinue |
        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
    }
}

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') -Row (Get-DataProperty $Data 'TopProcessesByMemory')
}

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)
    New-Finding -Category Memory -Check (Get-Text 'Check.Memory.RAMInUse') -Severity (Get-Severity $usedPercent $warn $fail) `
        -Value ((Get-Text 'Value.Memory.PercentWithFree') -f $usedPercent, ($free / 1MB)) `
        -Hint (Get-Text 'Hint.Memory.RAMFullConstantPagingSluggish')
}

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
}