Private/Kinds/BootPerformance.ps1

# The BootPerformance Kind: how long this machine takes to become usable.
#
# The number a Customer actually complains about, and the one Windows already measures for
# itself and never shows anyone. The list of what slowed the last boots down comes with it,
# because the duration alone tells a Technician there is a problem and not which one.

$script:BootPerformanceNeedsAdmin = $true

$script:BootPerformanceLog          = 'Microsoft-Windows-Diagnostics-Performance/Operational'
$script:BootPerformanceBootEventId  = 100
$script:BootPerformanceDegradationIds = @(101, 102, 103, 104, 105, 106, 107, 108, 109, 110)

# How many recent boots to average. One boot after a feature update is not this machine's
# normal, and a single reading would report it as though it were.
$script:BootPerformanceSampleCount = 5

function Get-BootPerformanceData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $days  = [int](Get-Parameter $Parameters 'Days' 30)
    $since = (Get-Date).AddDays(-$days)

    $unreadable = @{}

    $boots = @(Get-StabilityEvent -Log $script:BootPerformanceLog -Provider @() `
        -Id @($script:BootPerformanceBootEventId) -Since $since -Unreadable $unreadable |
        Sort-Object TimeCreated -Descending | Select-Object -First $script:BootPerformanceSampleCount |
        ForEach-Object {
            [pscustomobject]@{
                Time            = $_.TimeCreated
                BootMilliseconds = Get-EventPayload -LogEntry $_ -Index 0
            }
        })

    $degradations = @(Get-StabilityEvent -Log $script:BootPerformanceLog -Provider @() `
        -Id $script:BootPerformanceDegradationIds -Since $since -Unreadable $unreadable |
        ForEach-Object {
            $name = Get-EventPayload -LogEntry $_ -Index 0
            [pscustomobject]@{ Time = $_.TimeCreated; Item = $name }
        })

    [pscustomobject]@{
        PSTypeName    = 'Gutcheck.Data.BootPerformance'
        Days          = $days
        Boots         = $boots
        Degradations  = $degradations
        LogUnreadable = $unreadable.ContainsKey($script:BootPerformanceLog)
    }
}

function ConvertTo-BootPerformanceFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    $warn = Get-Parameter $Parameters 'BootDurationWarnAboveSeconds'  90
    $fail = Get-Parameter $Parameters 'BootDurationFailAboveSeconds' 180

    if (Get-DataProperty $Data 'LogUnreadable') {
        return New-Finding -Category System -Check (Get-Text 'Check.BootPerformance.BootDurationAvgLast') -Severity INFO `
            -Value (Get-Text 'Value.Shared.NeedsAdmin') `
            -Hint (Get-Text 'Hint.BootPerformance.RerunAndAllowTheAdmin')
    }

    $durations = @((Get-DataCollection $Data 'Boots') |
        ForEach-Object { ConvertTo-Number $_.BootMilliseconds } |
        Where-Object { $null -ne $_ -and $_ -gt 0 })

    if (-not $durations.Count) {
        # A machine that has not been restarted in the period, or one whose diagnostics
        # task has never run. Neither is a slow boot.
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.BootPerformance.BootDurationAvgLast') `
            -Hint (Get-Text 'Hint.BootPerformance.WindowsHasRecordedNoBoot')
    }

    $seconds = ($durations | Measure-Object -Average).Average / 1000

    New-Finding -Category System -Check (Get-Text 'Check.BootPerformance.BootDurationAvgLast') `
        -Severity (Get-Severity $seconds $warn $fail) -Value ('{0:N0} s' -f $seconds) `
        -Hint (Get-Text 'Hint.BootPerformance.SlowBootCheckStartupPrograms')
}

function ConvertTo-BootPerformanceSection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    $degradations = Get-DataCollection $Data 'Degradations'
    if (-not $degradations.Count) { return }

    New-Section -Title (Get-Text 'Title.BootPerformance.ItemsThatSlowedDown') -Row @(
        $degradations | Where-Object { $_.Item } | Group-Object Item |
            Sort-Object Count -Descending | Select-Object -First 15 Count, Name
    )
}