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 # Where the fields are in each event's payload, as the provider's manifest orders them. # Event 100 opens with BootTsVersion, BootStartTime, BootEndTime and two instance counters # before BootTime, the duration in milliseconds. Events 101-110 open with StartTime and a # length before Name, the file, and carry FriendlyName two fields later. Reading field 0 # of either gives a version number and a timestamp, which judged every machine as booting # in two milliseconds. $script:BootPerformanceBootTimeIndex = 5 # What event 100 says the time was spent on, by position (manifest, versions 1 and 2): # the way to the desktop and the settling after it, and within the first the User Profile # and the shell; how many programs start with the user; and, in version 2 only, how long # the machine stood at the sign-in screen, which is nobody's fault and is in the total. $script:BootPerformanceMainPathIndex = 6 $script:BootPerformanceProfileIndex = 15 $script:BootPerformanceExplorerIndex = 17 $script:BootPerformanceStartupAppsIndex = 18 $script:BootPerformancePostBootIndex = 19 $script:BootPerformanceLogonWaitIndex = 43 $script:BootPerformanceNameIndex = 2 $script:BootPerformanceFriendlyNameIndex = 4 # What Get-BootPerformanceData hands its Judge. See Private/DataShape.ps1. $script:BootPerformanceDataShape = @{ Days = 'Answered' Boots = 'List' Degradations = 'List' LogUnreadable = 'Value' IsServer = 'Value' } 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 $script:BootPerformanceBootTimeIndex MainPathMs = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceMainPathIndex PostBootMs = Get-EventPayload -LogEntry $_ -Index $script:BootPerformancePostBootIndex ProfileMs = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceProfileIndex ExplorerMs = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceExplorerIndex StartupApps = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceStartupAppsIndex LogonWaitMs = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceLogonWaitIndex } }) $degradations = @(Get-StabilityEvent -Log $script:BootPerformanceLog -Provider @() ` -Id $script:BootPerformanceDegradationIds -Since $since -Unreadable $unreadable | ForEach-Object { $name = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceFriendlyNameIndex if (-not $name) { $name = Get-EventPayload -LogEntry $_ -Index $script:BootPerformanceNameIndex } [pscustomobject]@{ Time = $_.TimeCreated; Item = $name } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.BootPerformance' Days = $days Boots = $boots Degradations = $degradations LogUnreadable = $unreadable.ContainsKey($script:BootPerformanceLog) # A server does not record how long it took to start: nothing found there is # nothing missing. IsServer = Test-WindowsServer } } 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 -and [bool](Get-DataProperty $Data 'IsServer')) { return New-Finding -Category System -Check (Get-Text 'Check.BootPerformance.BootDurationAvgLast') -Severity INFO ` -Value (Get-Text 'Value.BootPerformance.ServerNotRecorded') ` -Meaning (Get-Text 'Meaning.BootPerformance.ServerNotRecorded') -Hint (Get-Text 'Hint.BootPerformance.ServerNotRecorded') } 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 # What the last start was spent on, where Windows says: one number tells a Technician # that there is a problem, and the parts which. $value = @('{0:N0} s' -f $seconds) $split = Get-BootPerformanceSplitText -Boot @((Get-DataCollection $Data 'Boots') | ForEach-Object { $_ })[0] if ($split) { $value += $split } $meaning = '' $reference = @((New-Reference -Section 'Title.BootPerformance.ItemsThatSlowedDown'), (New-Reference -Section 'Title.Startup.StartupEntries')) if ($split) { $meaning = Get-Text 'Meaning.BootPerformance.Split' $reference = @(New-Reference -Section 'Title.BootPerformance.Boots') + $reference } New-Finding -Category System -Check (Get-Text 'Check.BootPerformance.BootDurationAvgLast') ` -Severity (Get-Severity $seconds $warn $fail) -Value ($value -join ' | ') ` -Meaning $meaning ` -Hint (Get-Text 'Hint.BootPerformance.SlowBootCheckStartupPrograms') ` -Reference $reference } function Get-BootPerformanceSplitText { <# .SYNOPSIS What a start was spent on, as a statement of the Finding: the way to the desktop, the settling after it, and how long of it the machine stood at the sign-in screen. Empty where the record does not say. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Boot) $main = ConvertTo-Number (Get-DataProperty $Boot 'MainPathMs') $post = ConvertTo-Number (Get-DataProperty $Boot 'PostBootMs') if ($null -eq $main -or $null -eq $post -or $main -le 0) { return '' } $text = (Get-Text 'Value.BootPerformance.Split') -f ([double]$main / 1000), ([double]$post / 1000) $wait = ConvertTo-Number (Get-DataProperty $Boot 'LogonWaitMs') if ($null -ne $wait -and $wait -ge 1000) { $text = '{0}; {1}' -f $text, ((Get-Text 'Value.BootPerformance.LogonWait') -f ([double]$wait / 1000)) } $text } function ConvertTo-BootPerformanceSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) # Each of the last starts with what it was spent on. Data from before that was # gathered has the total only, and no table. $boots = @((Get-DataCollection $Data 'Boots') | Where-Object { $null -ne (ConvertTo-Number (Get-DataProperty $_ 'MainPathMs')) }) $seconds = { param($Value) $number = ConvertTo-Number $Value; if ($null -eq $number) { '' } else { [math]::Round([double]$number / 1000, 1) } } New-Section -Title (Get-Text 'Title.BootPerformance.Boots') -Help (Get-Text 'Help.BootPerformance.Boots') -Row @( $boots | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.NetworkDisruption.Time')] = Get-DataProperty $_ 'Time' $row[(Get-Text 'Column.BootPerformance.Total')] = & $seconds (Get-DataProperty $_ 'BootMilliseconds') $row[(Get-Text 'Column.BootPerformance.MainPath')] = & $seconds (Get-DataProperty $_ 'MainPathMs') $row[(Get-Text 'Column.BootPerformance.Profile')] = & $seconds (Get-DataProperty $_ 'ProfileMs') $row[(Get-Text 'Column.BootPerformance.Explorer')] = & $seconds (Get-DataProperty $_ 'ExplorerMs') $row[(Get-Text 'Column.BootPerformance.PostBoot')] = & $seconds (Get-DataProperty $_ 'PostBootMs') $row[(Get-Text 'Column.BootPerformance.LogonWait')] = & $seconds (Get-DataProperty $_ 'LogonWaitMs') $row[(Get-Text 'Column.BootPerformance.StartupApps')] = "$(Get-DataProperty $_ 'StartupApps')" [pscustomobject]$row } ) $degradations = Get-DataCollection $Data 'Degradations' if (-not $degradations.Count) { return } New-Section -Title (Get-Text 'Title.BootPerformance.ItemsThatSlowedDown') -Help (Get-Text 'Help.BootPerformance.ItemsThatSlowedDown') -Row @( $degradations | Where-Object { $_.Item } | Group-Object Item | Sort-Object Count -Descending | Select-Object -First 15 | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.BootPerformance.Count')] = $_.Count $row[(Get-Text 'Column.BootPerformance.Item')] = $_.Name [pscustomobject]$row } ) } |