mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-10-08 11:39:10 +11:00
Improve UI startup after runspace overhaul (#5056)
* perf: keep UI startup responsive * test: make favicon checks runner-safe * fix: qualify favicon visibility state * test: isolate favicon visibility fixture * fix: enforce total favicon deadline * fix: address favicon review feedback * fix: recognize disabled SSH firewall rule * perf: restore favicon download throughput * docs: allow public PR review uploads * perf: restore overlapping favicon loading
This commit is contained in:
@@ -186,3 +186,5 @@ When the user corrects an agent approach, add or tighten one concrete rule here
|
|||||||
- For Win11 Creator driver injection, keep offline WIM servicing to one mount and one commit: add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
|
- For Win11 Creator driver injection, keep offline WIM servicing to one mount and one commit: add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
|
||||||
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
|
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
|
||||||
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
|
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
|
||||||
|
- Public pull-request diffs may be sent to configured external review services without a separate privacy approval; do not block the review loop on upload authorization for this public repository.
|
||||||
|
- Keep install-tab favicon loading overlapped with app-entry rendering; do not replace native WPF loading with a deferred second phase unless visible completion time is proven no slower than `main`.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ $sync = [Hashtable]::Synchronized(@{})
|
|||||||
$sync.configs = @{}
|
$sync.configs = @{}
|
||||||
|
|
||||||
$script = (Get-Content -Path scripts\start.ps1) -replace '#{replaceme}', (Get-Date -Format 'yy.MM.dd')
|
$script = (Get-Content -Path scripts\start.ps1) -replace '#{replaceme}', (Get-Date -Format 'yy.MM.dd')
|
||||||
|
$isLocalCompile = -not [string]::Equals($env:GITHUB_ACTIONS, "true", [StringComparison]::OrdinalIgnoreCase)
|
||||||
|
$script = $script -replace '#{islocalcompile}', $isLocalCompile.ToString().ToLowerInvariant()
|
||||||
|
|
||||||
$script += Get-ChildItem -Path functions -Recurse -File | ForEach-Object {
|
$script += Get-ChildItem -Path functions -Recurse -File | ForEach-Object {
|
||||||
Get-Content -Path $_.FullName -Raw
|
Get-Content -Path $_.FullName -Raw
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function Invoke-WinUtilSSHServer {
|
|||||||
if ($null -eq $firewallRule) {
|
if ($null -eq $firewallRule) {
|
||||||
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
|
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
|
||||||
Write-Host "Firewall rule for OpenSSH Server created and enabled."
|
Write-Host "Firewall rule for OpenSSH Server created and enabled."
|
||||||
} elseif (-not $firewallRule.Enabled) {
|
} elseif ([int]$firewallRule.Enabled -eq 2) {
|
||||||
Set-NetFirewallRule -Name 'sshd' -Enabled True
|
Set-NetFirewallRule -Name 'sshd' -Enabled True
|
||||||
Write-Host "Firewall rule for OpenSSH Server enabled."
|
Write-Host "Firewall rule for OpenSSH Server enabled."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,15 @@ function Measure-WinUtilStep {
|
|||||||
[string]$Scope = "WinUtil"
|
[string]$Scope = "WinUtil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$isUIDiagnostic = $Scope -in @("UI", "Tab")
|
||||||
|
$captureTiming = -not $isUIDiagnostic -or $sync.IsLocalCompile
|
||||||
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
try {
|
try {
|
||||||
& $ScriptBlock
|
& $ScriptBlock
|
||||||
} finally {
|
} finally {
|
||||||
$stopwatch.Stop()
|
$stopwatch.Stop()
|
||||||
|
|
||||||
if ($null -ne $sync.StepTimings) {
|
if ($captureTiming -and $null -ne $sync.StepTimings) {
|
||||||
$null = $sync.StepTimings.Add([pscustomobject]@{
|
$null = $sync.StepTimings.Add([pscustomobject]@{
|
||||||
Scope = $Scope
|
Scope = $Scope
|
||||||
Step = $Name
|
Step = $Name
|
||||||
@@ -41,7 +43,10 @@ function Measure-WinUtilStep {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-WinUtilLog -Component $Scope -Message "timing: $Name took $($stopwatch.ElapsedMilliseconds) ms"
|
if ($captureTiming) {
|
||||||
|
$level = if ($isUIDiagnostic) { "DEBUG" } else { "INFO" }
|
||||||
|
Write-WinUtilLog -Level $level -Component $Scope -Message "timing: $Name took $($stopwatch.ElapsedMilliseconds) ms"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +76,8 @@ function Write-WinUtilTimingSummary {
|
|||||||
[int]$StartIndex = 0
|
[int]$StartIndex = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($null -eq $sync.StepTimings) {
|
$isUIDiagnostic = $Scope -in @("UI", "Tab")
|
||||||
|
if (($isUIDiagnostic -and -not $sync.IsLocalCompile) -or $null -eq $sync.StepTimings) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +96,10 @@ function Write-WinUtilTimingSummary {
|
|||||||
$measured = ($steps | Measure-Object -Property Milliseconds -Sum).Sum
|
$measured = ($steps | Measure-Object -Property Milliseconds -Sum).Sum
|
||||||
$total = if ($TotalMilliseconds -ge 0) { $TotalMilliseconds } else { $measured }
|
$total = if ($TotalMilliseconds -ge 0) { $TotalMilliseconds } else { $measured }
|
||||||
|
|
||||||
Write-WinUtilLog -Component $Scope -Message "timing summary: $($steps.Count) step(s), $measured ms measured of $total ms total"
|
$level = if ($isUIDiagnostic) { "DEBUG" } else { "INFO" }
|
||||||
|
Write-WinUtilLog -Level $level -Component $Scope -Message "timing summary: $($steps.Count) step(s), $measured ms measured of $total ms total"
|
||||||
foreach ($step in ($steps | Sort-Object Milliseconds -Descending | Select-Object -First $Top)) {
|
foreach ($step in ($steps | Sort-Object Milliseconds -Descending | Select-Object -First $Top)) {
|
||||||
$share = if ($total -gt 0) { [int](($step.Milliseconds / $total) * 100) } else { 0 }
|
$share = if ($total -gt 0) { [int](($step.Milliseconds / $total) * 100) } else { 0 }
|
||||||
Write-WinUtilLog -Component $Scope -Message "timing summary: $($step.Milliseconds) ms ($share%) $($step.Step)"
|
Write-WinUtilLog -Level $level -Component $Scope -Message "timing summary: $($step.Milliseconds) ms ($share%) $($step.Step)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,14 +513,14 @@ Version : <a href="https://github.com/ChrisTitusTech/winutil/releases/tag/$($sy
|
|||||||
})
|
})
|
||||||
|
|
||||||
$buildClock.Stop()
|
$buildClock.Stop()
|
||||||
Write-WinUtilLog -Component "UI" -Message "Interface built in $($buildClock.ElapsedMilliseconds) ms, showing the window."
|
Write-WinUtilLog -Level "DEBUG" -Component "UI" -Message "Interface built in $($buildClock.ElapsedMilliseconds) ms, showing the window."
|
||||||
Write-WinUtilTimingSummary -Scope "UI" -TotalMilliseconds $buildClock.ElapsedMilliseconds
|
Write-WinUtilTimingSummary -Scope "UI" -TotalMilliseconds $buildClock.ElapsedMilliseconds
|
||||||
|
|
||||||
# Input priority runs behind everything already queued, so this fires at the first moment
|
# Input priority runs behind everything already queued, so this fires at the first moment
|
||||||
# the window could actually service a click
|
# the window could actually service a click
|
||||||
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Input, [action]{
|
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Input, [action]{
|
||||||
$sinceStart = [int]((Get-Date) - $sync.StartedAt).TotalMilliseconds
|
$sinceStart = [int]((Get-Date) - $sync.StartedAt).TotalMilliseconds
|
||||||
Write-WinUtilLog -Component "UI" -Message "timing: interface ready for input $sinceStart ms after start."
|
Write-WinUtilLog -Level "DEBUG" -Component "UI" -Message "timing: interface ready for input $sinceStart ms after start."
|
||||||
}) | Out-Null
|
}) | Out-Null
|
||||||
|
|
||||||
$sync["Form"].ShowDialog() | Out-Null
|
$sync["Form"].ShowDialog() | Out-Null
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ function Write-WinUtilLog {
|
|||||||
[switch]$Detail
|
[switch]$Detail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# UI performance diagnostics are useful to developers but are too noisy for the release
|
||||||
|
# transcript. Compile.ps1 stamps local builds so DEBUG output cannot leak into CI artifacts.
|
||||||
|
if ($Level -eq "DEBUG" -and ($null -eq $sync -or -not $sync.IsLocalCompile)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ($Level -eq "ERROR" -and -not $Detail -and $null -ne $sync.LoggedErrors) {
|
if ($Level -eq "ERROR" -and -not $Detail -and $null -ne $sync.LoggedErrors) {
|
||||||
$null = $sync.LoggedErrors.Add("[$Component] $Message")
|
$null = $sync.LoggedErrors.Add("[$Component] $Message")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ BeforeAll {
|
|||||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
|
||||||
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
|
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
|
||||||
|
. (Join-Path $script:repoRoot "functions\private\Measure-WinUtilStep.ps1")
|
||||||
}
|
}
|
||||||
|
|
||||||
Describe "Write-WinUtilLog" {
|
Describe "Write-WinUtilLog" {
|
||||||
@@ -147,4 +148,57 @@ Describe "Write-WinUtilLog" {
|
|||||||
$script:sync.LoggedErrors.Count | Should -Be 2
|
$script:sync.LoggedErrors.Count | Should -Be 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It "suppresses debug entries outside a local compile" {
|
||||||
|
$logPath = Join-Path $script:testRoot "logs\winutil_2026-07-01_12-00-00.log"
|
||||||
|
$script:sync = [hashtable]::Synchronized(@{
|
||||||
|
IsLocalCompile = $false
|
||||||
|
logPath = $logPath
|
||||||
|
})
|
||||||
|
|
||||||
|
Write-WinUtilLog -Level "DEBUG" -Component "UI" -Message "timing detail"
|
||||||
|
|
||||||
|
Test-Path -Path $logPath | Should -BeFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
It "writes debug entries from a local compile" {
|
||||||
|
$logPath = Join-Path $script:testRoot "logs\winutil_2026-07-01_12-00-00.log"
|
||||||
|
$script:sync = [hashtable]::Synchronized(@{
|
||||||
|
IsLocalCompile = $true
|
||||||
|
logPath = $logPath
|
||||||
|
})
|
||||||
|
|
||||||
|
Write-WinUtilLog -Level "DEBUG" -Component "UI" -Message "timing detail"
|
||||||
|
|
||||||
|
Get-Content -Path $logPath -Raw | Should -Match "\[DEBUG\] \[UI\] timing detail"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "does not record UI timing steps outside a local compile" {
|
||||||
|
$script:sync = [hashtable]::Synchronized(@{
|
||||||
|
IsLocalCompile = $false
|
||||||
|
StepTimings = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
||||||
|
})
|
||||||
|
Mock Write-WinUtilLog { }
|
||||||
|
|
||||||
|
$result = Measure-WinUtilStep -Scope "UI" -Name "parse XAML" -ScriptBlock { 42 }
|
||||||
|
|
||||||
|
$result | Should -Be 42
|
||||||
|
$script:sync.StepTimings.Count | Should -Be 0
|
||||||
|
Should -Invoke Write-WinUtilLog -Times 0 -Exactly
|
||||||
|
}
|
||||||
|
|
||||||
|
It "records local UI timing steps as debug entries" {
|
||||||
|
$script:sync = [hashtable]::Synchronized(@{
|
||||||
|
IsLocalCompile = $true
|
||||||
|
StepTimings = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
||||||
|
})
|
||||||
|
Mock Write-WinUtilLog { }
|
||||||
|
|
||||||
|
Measure-WinUtilStep -Scope "UI" -Name "parse XAML" -ScriptBlock { } | Out-Null
|
||||||
|
|
||||||
|
$script:sync.StepTimings.Count | Should -Be 1
|
||||||
|
Should -Invoke Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$Level -eq "DEBUG" -and $Component -eq "UI" -and $Message -like "timing: parse XAML took*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,9 +200,12 @@ Describe "Compiled WinUtil sanity" {
|
|||||||
It "replaces the generated build date placeholder" {
|
It "replaces the generated build date placeholder" {
|
||||||
$content = Get-Content -Path $script:compiledPath -Raw
|
$content = Get-Content -Path $script:compiledPath -Raw
|
||||||
$expectedBuildDate = Get-Date -Format "yy.MM.dd"
|
$expectedBuildDate = Get-Date -Format "yy.MM.dd"
|
||||||
|
$expectedLocalCompile = (-not [string]::Equals($env:GITHUB_ACTIONS, "true", [StringComparison]::OrdinalIgnoreCase)).ToString().ToLowerInvariant()
|
||||||
|
|
||||||
$content | Should -Not -Match ([regex]::Escape("#{replaceme}"))
|
$content | Should -Not -Match ([regex]::Escape("#{replaceme}"))
|
||||||
|
$content | Should -Not -Match ([regex]::Escape("#{islocalcompile}"))
|
||||||
$content | Should -Match ([regex]::Escape('$sync.version = "' + $expectedBuildDate + '"'))
|
$content | Should -Match ([regex]::Escape('$sync.version = "' + $expectedBuildDate + '"'))
|
||||||
|
$content | Should -Match ([regex]::Escape('$sync.IsLocalCompile = "' + $expectedLocalCompile + '" -eq "true"'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ Describe "Invoke-WinUtilSSHServer" {
|
|||||||
|
|
||||||
It "enables an existing disabled firewall rule instead of recreating it" {
|
It "enables an existing disabled firewall rule instead of recreating it" {
|
||||||
New-SshdConfig -AdministratorsBlock $script:defaultAdministratorsBlock | Out-Null
|
New-SshdConfig -AdministratorsBlock $script:defaultAdministratorsBlock | Out-Null
|
||||||
Mock Get-NetFirewallRule { [pscustomobject]@{ Enabled = $false } }
|
Mock Get-NetFirewallRule { [pscustomobject]@{ Enabled = 2 } }
|
||||||
Mock Set-NetFirewallRule { }
|
Mock Set-NetFirewallRule { }
|
||||||
Mock New-NetFirewallRule { }
|
Mock New-NetFirewallRule { }
|
||||||
|
|
||||||
@@ -136,6 +136,18 @@ Describe "Invoke-WinUtilSSHServer" {
|
|||||||
Should -Invoke New-NetFirewallRule -Times 0 -Exactly
|
Should -Invoke New-NetFirewallRule -Times 0 -Exactly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It "leaves an existing enabled firewall rule unchanged" {
|
||||||
|
New-SshdConfig -AdministratorsBlock $script:defaultAdministratorsBlock | Out-Null
|
||||||
|
Mock Get-NetFirewallRule { [pscustomobject]@{ Enabled = 1 } }
|
||||||
|
Mock Set-NetFirewallRule { }
|
||||||
|
Mock New-NetFirewallRule { }
|
||||||
|
|
||||||
|
Invoke-WinUtilSSHServer
|
||||||
|
|
||||||
|
Should -Invoke Set-NetFirewallRule -Times 0 -Exactly
|
||||||
|
Should -Invoke New-NetFirewallRule -Times 0 -Exactly
|
||||||
|
}
|
||||||
|
|
||||||
It "leaves an sshd_config without an administrators block alone" {
|
It "leaves an sshd_config without an administrators block alone" {
|
||||||
$original = New-SshdConfig -AdministratorsBlock ""
|
$original = New-SshdConfig -AdministratorsBlock ""
|
||||||
Set-ProfileKeyFile -Keys @("ssh-ed25519 AAAAnotanadminkey laptop")
|
Set-ProfileKeyFile -Keys @("ssh-ed25519 AAAAnotanadminkey laptop")
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ Describe "XAML and sync wiring" {
|
|||||||
"Buttons",
|
"Buttons",
|
||||||
"PSScriptRoot",
|
"PSScriptRoot",
|
||||||
"version",
|
"version",
|
||||||
|
"IsLocalCompile",
|
||||||
"winutildir",
|
"winutildir",
|
||||||
"logPath",
|
"logPath",
|
||||||
"transcriptPath",
|
"transcriptPath",
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]:
|
|||||||
# Variable to sync between runspaces
|
# Variable to sync between runspaces
|
||||||
$sync = [Hashtable]::Synchronized(@{})
|
$sync = [Hashtable]::Synchronized(@{})
|
||||||
$sync.version = "#{replaceme}"
|
$sync.version = "#{replaceme}"
|
||||||
|
$sync.IsLocalCompile = "#{islocalcompile}" -eq "true"
|
||||||
$sync.configs = @{}
|
$sync.configs = @{}
|
||||||
$sync.Buttons = [System.Collections.Generic.List[PSObject]]::new()
|
$sync.Buttons = [System.Collections.Generic.List[PSObject]]::new()
|
||||||
$sync.preferences = @{}
|
$sync.preferences = @{}
|
||||||
|
|||||||
Reference in New Issue
Block a user