Speed up runspace and tab initialization (#4793)

* Add startup performance tracing

* Lazy initialize non-default tabs

* Render install app entries incrementally

* Defer and cache toggle status checks

* Clean up runspace invocation ownership

* Defer GUI runspace pool startup

* Defer status taskbar asset rendering

* Record final speed verification

* Fix deferred install render timer callback

* Add dispatcher smoke coverage for install rendering

* Replace install render timer with dispatcher callbacks

* Gate performance tracing behind compile switch

* Clean up analyzer warnings in speed changes

* Speed up runspace and tab initialization
This commit is contained in:
Chris Titus
2026-07-01 23:34:59 -05:00
committed by GitHub
parent 916dc761ba
commit 93dc23dd66
31 changed files with 1114 additions and 133 deletions
+42
View File
@@ -0,0 +1,42 @@
#===========================================================================
# Tests - Asset rendering
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
}
Describe "Rendered asset caching" {
It "caches rendered bitmap assets by type and size" {
$assetScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilAssets.ps1") -Raw
$assetScript | Should -Match 'RenderedAssetCache'
$assetScript | Should -Match '\$cacheKey = "\$\(\(\[string\]\$type\)\.ToLowerInvariant\(\)\)\|\$Size"'
$assetScript | Should -Match 'return \$sync\.RenderedAssetCache\[\$cacheKey\]'
$assetScript | Should -Match '\$sync\.RenderedAssetCache\[\$cacheKey\] = \$bitmapImage'
}
It "renders only the logo overlay before first paint and defers status overlays" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$mainScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$true -IncludeStatusAssets \$false'
$mainScript | Should -Match 'Dispatcher\.BeginInvoke\(\[System\.Windows\.Threading\.DispatcherPriority\]::Background, \[action\]\{ Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$false -IncludeStatusAssets \$true \}'
$mainScript | Should -Not -Match '\$sync\["checkmarkrender"\] = \(Invoke-WinUtilAssets -Type "checkmark"'
$mainScript | Should -Not -Match '\$sync\["warningrender"\] = \(Invoke-WinUtilAssets -Type "warning"'
}
It "lazily creates taskbar overlays before assigning them" {
$taskbarScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Set-WinUtilTaskbarItem.ps1") -Raw
$taskbarScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$true -IncludeStatusAssets \$false'
$taskbarScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$false -IncludeStatusAssets \$true'
}
It "records individual taskbar overlay render checkpoints" {
$overlayScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTaskbarOverlayAssets.ps1") -Raw
$overlayScript | Should -Match 'Taskbar logo asset rendered'
$overlayScript | Should -Match 'Taskbar checkmark asset rendered'
$overlayScript | Should -Match 'Taskbar warning asset rendered'
}
}
+3 -2
View File
@@ -245,13 +245,14 @@ Describe "Preset config" {
Describe "App navigation config" {
It "is wired to an existing XAML target grid" {
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$tabInitializerScript = Get-Content -Path (Join-Path $script:repoRoot "functions/private/Initialize-WinUtilTabContent.ps1") -Raw
$targetGridMatch = [regex]::Match(
$mainScript,
"$mainScript`n$tabInitializerScript",
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
)
if (-not $targetGridMatch.Success) {
throw "scripts/main.ps1 does not wire appnavigation through Invoke-WPFUIElements."
throw "Startup tab initialization does not wire appnavigation through Invoke-WPFUIElements."
}
$xamlText = Get-Content -Path $script:xamlPath -Raw
+134
View File
@@ -0,0 +1,134 @@
#===========================================================================
# Tests - Install tab rendering
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
}
Describe "Install app rendering startup contract" {
It "queues app entries after creating category containers" {
$categoryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallCategoryAppList.ps1") -Raw
$categoryScript | Should -Match '\$sync\.InstallAppRenderQueue = \[System\.Collections\.Queue\]::new\(\)'
$categoryScript | Should -Match 'Start-WinUtilInstallAppRendering'
$categoryScript | Should -Match 'Pre-group apps by category before creating WPF controls'
}
It "renders queued apps through dispatcher callbacks when a form dispatcher exists" {
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
$renderScript | Should -Match 'Dispatcher\.BeginInvoke'
$renderScript | Should -Match 'Invoke-WinUtilInstallAppRenderNextBatch'
$renderScript | Should -Match 'Initialize-InstallAppEntry'
$renderScript | Should -Match 'Install app entries rendered'
}
It "does not use dispatcher timers for deferred install rendering" {
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
$renderScript | Should -Not -Match 'DispatcherTimer'
$renderScript | Should -Not -Match '\$timer'
$renderScript | Should -Not -Match '\$dispatcherTimer'
$renderScript | Should -Not -Match '\$timer\.Stop\(\)'
$renderScript | Should -Not -Match '& \$renderCategory'
}
It "drains queued app batches on the WPF dispatcher without timer scope errors" {
Add-Type -AssemblyName WindowsBase
. (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1")
$previousSync = Get-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
$previousInitializeAppEntry = Get-Item -Path Function:\Initialize-InstallAppEntry -ErrorAction SilentlyContinue
$previousSearch = Get-Item -Path Function:\Find-AppsByNameOrDescription -ErrorAction SilentlyContinue
$previousCheckpoint = Get-Item -Path Function:\Write-WinUtilPerformanceCheckpoint -ErrorAction SilentlyContinue
$errorCountBefore = $global:Error.Count
try {
$global:sync = [Hashtable]::Synchronized(@{})
$global:sync.currentTab = "Install"
$global:sync.SearchBar = [pscustomobject]@{ Text = "" }
$global:sync.Form = [pscustomobject]@{ Dispatcher = [System.Windows.Threading.Dispatcher]::CurrentDispatcher }
$global:sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
$renderedApps = [System.Collections.Generic.List[string]]::new()
$checkpoints = [System.Collections.Generic.List[string]]::new()
function global:Initialize-InstallAppEntry {
param($TargetElement, $AppKey)
$renderedApps.Add($AppKey)
return "entry:$AppKey"
}
function global:Find-AppsByNameOrDescription {
param($SearchString)
throw "Search should not run for an empty search box in this test."
}
function global:Write-WinUtilPerformanceCheckpoint {
param([string]$Name)
$checkpoints.Add($Name)
}
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppA", "AppB") })
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppC") })
$frame = New-Object System.Windows.Threading.DispatcherFrame
$timeout = [System.Diagnostics.Stopwatch]::StartNew()
Start-WinUtilInstallAppRendering
$closeTimer = New-Object System.Windows.Threading.DispatcherTimer
$closeTimer.Interval = [TimeSpan]::FromMilliseconds(25)
$closeTimer.Add_Tick({
param($sender)
$timer = [System.Windows.Threading.DispatcherTimer]$sender
if ($global:sync.InstallAppEntriesRendered -or $timeout.Elapsed.TotalSeconds -gt 5) {
$timer.Stop()
$frame.Continue = $false
}
})
$closeTimer.Start()
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
$global:sync.InstallAppEntriesRendered | Should -BeTrue
$global:sync.InstallAppRenderQueue.Count | Should -Be 0
@($renderedApps) | Should -Be @("AppA", "AppB", "AppC")
@($checkpoints) | Should -Contain "Install app entries rendered"
$global:Error.Count | Should -Be $errorCountBefore
} finally {
if ($previousSync) {
Set-Variable -Name sync -Value $previousSync.Value -Scope Global
} else {
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
foreach ($functionBackup in @(
@{ Name = "Initialize-InstallAppEntry"; Backup = $previousInitializeAppEntry },
@{ Name = "Find-AppsByNameOrDescription"; Backup = $previousSearch },
@{ Name = "Write-WinUtilPerformanceCheckpoint"; Backup = $previousCheckpoint }
)) {
if ($functionBackup.Backup) {
Set-Item -Path "Function:\$($functionBackup.Name)" -Value $functionBackup.Backup.ScriptBlock
} else {
Remove-Item -Path "Function:\$($functionBackup.Name)" -ErrorAction SilentlyContinue
}
}
}
}
It "keeps app-entry metadata lookup independent from the old caller scope" {
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
$entryScript | Should -Match '\$app = \$sync\.configs\.applicationsHashtable\.\$appKey'
$entryScript | Should -Not -Match '\$Apps\.\$appKey'
}
It "restores delayed app checkbox state from selected apps" {
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
$entryScript | Should -Match '\$sync\.selectedApps -contains \$appKey'
$entryScript | Should -Match '\$checkBox\.IsChecked = \$true'
}
}
+94
View File
@@ -0,0 +1,94 @@
#===========================================================================
# Tests - Lazy tab initialization
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
function Invoke-WPFUIElements {
param($configVariable, [string]$targetGridName, [int]$columncount)
}
function Initialize-WPFUI {
param([string]$TargetGridName)
}
function Write-WinUtilPerformanceCheckpoint {
param([string]$Name)
}
function Invoke-WinUtilISOCheckExistingWork { }
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTabContent.ps1")
}
Describe "Initialize-WinUtilTabContent" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = @{
appnavigation = [pscustomobject]@{}
tweaks = [pscustomobject]@{}
feature = [pscustomobject]@{}
appx = [pscustomobject]@{}
}
})
Mock Invoke-WPFUIElements { }
Mock Initialize-WPFUI { }
Mock Write-WinUtilPerformanceCheckpoint { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "initializes the install tab once" {
Initialize-WinUtilTabContent -TabName "Install"
Initialize-WinUtilTabContent -TabName "Install"
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$targetGridName -eq "appscategory" -and $columncount -eq 1
}
Should -Invoke -CommandName Initialize-WPFUI -Times 1 -Exactly -ParameterFilter {
$TargetGridName -eq "appscategory"
}
Should -Invoke -CommandName Initialize-WPFUI -Times 1 -Exactly -ParameterFilter {
$TargetGridName -eq "appspanel"
}
$script:sync.InitializedTabs["Install"] | Should -BeTrue
}
It "initializes deferred config-backed tabs once" {
Initialize-WinUtilTabContent -TabName "Tweaks"
Initialize-WinUtilTabContent -TabName "Config"
Initialize-WinUtilTabContent -TabName "AppX"
Initialize-WinUtilTabContent -TabName "Tweaks"
Initialize-WinUtilTabContent -TabName "Config"
Initialize-WinUtilTabContent -TabName "AppX"
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$targetGridName -eq "tweakspanel" -and $columncount -eq 2
}
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$targetGridName -eq "featurespanel" -and $columncount -eq 2
}
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$targetGridName -eq "appxpanel" -and $columncount -eq 2
}
}
}
Describe "Startup lazy tab wiring" {
It "builds only install tab content before first paint" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$startupRegion = $mainScript.Substring(0, $mainScript.IndexOf("# Store Form Objects In PowerShell"))
$startupRegion | Should -Match 'Initialize-WinUtilTabContent -TabName "Install"'
$startupRegion | Should -Not -Match 'targetGridName "tweakspanel"'
$startupRegion | Should -Not -Match 'targetGridName "featurespanel"'
$startupRegion | Should -Not -Match 'targetGridName "appxpanel"'
}
It "initializes tab content when a tab is selected" {
$tabScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFTab.ps1") -Raw
$tabScript | Should -Match 'Initialize-WinUtilTabContent -TabName \$sync\.currentTab'
}
}
+116
View File
@@ -0,0 +1,116 @@
#===========================================================================
# Tests - Performance tracing
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
. (Join-Path $script:repoRoot "tools\perf\Test-WinUtilPerformanceTrace.ps1")
. (Join-Path $script:repoRoot "tools\perf\Write-WinUtilPerformanceCheckpoint.ps1")
. (Join-Path $script:repoRoot "tools\perf\Start-WinUtilPerformanceTrace.ps1")
. (Join-Path $script:repoRoot "tools\perf\Stop-WinUtilPerformanceTrace.ps1")
}
AfterAll {
& (Join-Path $script:repoRoot "Compile.ps1")
}
Describe "WinUtil performance tracing helpers" {
BeforeEach {
$script:originalPerfEnv = $env:WINUTIL_PERF_LOG
Remove-Item Env:\WINUTIL_PERF_LOG -ErrorAction SilentlyContinue
$script:sync = [Hashtable]::Synchronized(@{})
}
AfterEach {
if ($null -eq $script:originalPerfEnv) {
Remove-Item Env:\WINUTIL_PERF_LOG -ErrorAction SilentlyContinue
} else {
$env:WINUTIL_PERF_LOG = $script:originalPerfEnv
}
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name originalPerfEnv -Scope Script -ErrorAction SilentlyContinue
}
It "is disabled by default" {
Test-WinUtilPerformanceTrace | Should -BeFalse
}
It "can be enabled by environment variable" {
$env:WINUTIL_PERF_LOG = "1"
Test-WinUtilPerformanceTrace | Should -BeTrue
}
It "writes startup checkpoints through the normal WinUtil log helper" {
$env:WINUTIL_PERF_LOG = "1"
Mock Write-WinUtilLog { }
Start-WinUtilPerformanceTrace
Write-WinUtilPerformanceCheckpoint -Name "XAML loaded"
Stop-WinUtilPerformanceTrace
Should -Invoke -CommandName Write-WinUtilLog -Times 3 -Exactly -ParameterFilter {
$Component -eq "StartupPerf" -and $Level -eq "DEBUG"
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Message -like "XAML loaded:*"
}
}
}
Describe "Startup performance checkpoints" {
It "keeps performance tracing out of normal compiled output" {
& (Join-Path $script:repoRoot "Compile.ps1")
$compiledScript = Get-Content -Path (Join-Path $script:repoRoot "winutil.ps1") -Raw
$compiledScript | Should -Not -Match "Test-WinUtilPerformanceTrace"
$compiledScript | Should -Not -Match "Start-WinUtilPerformanceTrace"
$compiledScript | Should -Not -Match "Write-WinUtilPerformanceCheckpoint"
$compiledScript | Should -Not -Match "Stop-WinUtilPerformanceTrace"
$compiledScript | Should -Not -Match "PerformanceTraceEnabled"
}
It "adds config-load checkpoints only to trace compiled output" {
$compileScript = Get-Content -Path (Join-Path $script:repoRoot "Compile.ps1") -Raw
$compileScript | Should -Match '\[switch\]\$Trace'
$compileScript | Should -Match "Start-WinUtilPerformanceTrace"
$compileScript | Should -Match "Config load start"
$compileScript | Should -Match "Config load complete"
$compileScript | Should -Match "Config .* loaded"
& (Join-Path $script:repoRoot "Compile.ps1") -Trace
$compiledScript = Get-Content -Path (Join-Path $script:repoRoot "winutil.ps1") -Raw
$compiledScript | Should -Match "function Test-WinUtilPerformanceTrace"
$compiledScript | Should -Match '\$sync\.PerformanceTraceEnabled = \$true'
$compiledScript | Should -Match "Config load start"
$compiledScript | Should -Match "Config load complete"
}
It "adds runtime checkpoints for startup hotspots" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$lazyTabScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTabContent.ps1") -Raw
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1") -Raw
$overlayScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTaskbarOverlayAssets.ps1") -Raw
$startupText = "$mainScript`n$lazyTabScript`n$runspaceScript`n$overlayScript"
foreach ($checkpoint in @(
"Runspace pool initialized",
"XAML loaded",
"Theme applied",
"Install UI created",
"Tweaks UI created",
"Features UI created",
"AppX UI created",
"Taskbar logo asset rendered",
"First content rendered"
)) {
$startupText | Should -Match ([regex]::Escape($checkpoint))
}
}
}
+70
View File
@@ -0,0 +1,70 @@
#===========================================================================
# Tests - Runspace lifecycle
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
}
Describe "Initialize-WinUtilRunspacePool" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{})
$script:PARAM_OFFLINE = $false
function Write-WinUtilPerformanceCheckpoint { param($Name) }
Mock Write-WinUtilPerformanceCheckpoint { }
}
AfterEach {
Close-WinUtilRunspacePool
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name PARAM_OFFLINE -Scope Script -ErrorAction SilentlyContinue
}
It "creates and reuses one open runspace pool" {
$firstPool = Initialize-WinUtilRunspacePool
$secondPool = Initialize-WinUtilRunspacePool
$firstPool.RunspacePoolStateInfo.State | Should -Be ([System.Management.Automation.Runspaces.RunspacePoolState]::Opened)
[object]::ReferenceEquals($firstPool, $secondPool) | Should -BeTrue
Should -Invoke -CommandName Write-WinUtilPerformanceCheckpoint -Times 1 -Exactly -ParameterFilter {
$Name -eq "Runspace pool initialized"
}
}
It "closes and removes the active runspace pool" {
$pool = Initialize-WinUtilRunspacePool
Close-WinUtilRunspacePool
$pool.RunspacePoolStateInfo.State | Should -Be ([System.Management.Automation.Runspaces.RunspacePoolState]::Closed)
$script:sync.ContainsKey("runspace") | Should -BeFalse
}
}
Describe "Runspace startup wiring" {
It "does not create the GUI runspace pool before automation checks" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$beforePreset = $mainScript.Substring(0, $mainScript.IndexOf('if ($Preset)'))
$beforePreset | Should -Not -Match '\[runspacefactory\]::CreateRunspacePool'
$beforePreset | Should -Not -Match '\$sync\.runspace\.Open\(\)'
}
It "initializes runspaces synchronously for automation paths and after first render for GUI" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$mainScript | Should -Match 'if \(\$Preset\) \{\s+Initialize-WinUtilRunspacePool'
$mainScript | Should -Match 'if \(\$Config\) \{\s+Initialize-WinUtilRunspacePool'
$mainScript | Should -Match 'Dispatcher\.BeginInvoke\(\[System\.Windows\.Threading\.DispatcherPriority\]::Background, \[action\]\{ Initialize-WinUtilRunspacePool'
$mainScript | Should -Match 'Close-WinUtilRunspacePool'
}
It "creates runspaces on demand before queueing background work" {
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1") -Raw
$runspaceScript | Should -Match 'Initialize-WinUtilRunspacePool \| Out-Null'
}
}
+54 -14
View File
@@ -4,6 +4,8 @@
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
@@ -18,21 +20,17 @@ BeforeAll {
$initialSessionState.Variables.Add($syncVariable)
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
$script:sync.runspace.Open()
function Write-WinUtilPerformanceCheckpoint { param($Name) }
}
function script:Clear-WinUtilRunspaceTestContext {
if ($script:powershell) {
$script:powershell.Dispose()
}
if ($script:sync -and $script:sync.runspace) {
$script:sync.runspace.Close()
$script:sync.runspace.Dispose()
}
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
}
function script:Assert-WinUtilAsyncHandle {
@@ -54,28 +52,34 @@ Describe "Invoke-WPFRunspace behavior" {
}
It "returns a single async handle with no argument list" {
$script:sync.Result = $null
$handle = Invoke-WPFRunspace -ScriptBlock {
Start-Sleep -Milliseconds 100
"no-args|$($sync.Marker)"
$sync.Result = "no-args|$($sync.Marker)"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "no-args|shared"
$script:sync.Result | Should -Be "no-args|shared"
}
It "passes one named parameter" {
$script:sync.Result = $null
$handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
param([string]$Name)
Start-Sleep -Milliseconds 100
"Name=$Name"
$sync.Result = "Name=$Name"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "Name=value"
$script:sync.Result | Should -Be "Name=value"
}
It "passes multiple named parameters" {
$script:sync.Result = $null
$handle = Invoke-WPFRunspace -ParameterList @(
("First", "alpha"),
("Second", "beta")
@@ -86,21 +90,57 @@ Describe "Invoke-WPFRunspace behavior" {
)
Start-Sleep -Milliseconds 100
"$First|$Second|$($sync.Marker)"
$sync.Result = "$First|$Second|$($sync.Marker)"
}
Assert-WinUtilAsyncHandle -Handle $handle
@($script:powershell.EndInvoke($handle))[0] | Should -Be "alpha|beta|shared"
$script:sync.Result | Should -Be "alpha|beta|shared"
}
It "surfaces scriptblock failures through the owning PowerShell instance" {
It "keeps the shared runspace pool usable after scriptblock failures" {
$handle = Invoke-WPFRunspace -ScriptBlock {
Start-Sleep -Milliseconds 100
throw "runspace failure"
}
Assert-WinUtilAsyncHandle -Handle $handle
{ $script:powershell.EndInvoke($handle) } | Should -Throw -ExpectedMessage "*runspace failure*"
$script:sync.Result = $null
$secondHandle = Invoke-WPFRunspace -ScriptBlock {
$sync.Result = "after-failure"
}
Assert-WinUtilAsyncHandle -Handle $secondHandle
$script:sync.Result | Should -Be "after-failure"
}
It "runs multiple queued invocations without shared PowerShell state" {
$script:sync.FirstResult = $null
$script:sync.SecondResult = $null
$firstHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "first")) -ScriptBlock {
param([string]$Value)
Start-Sleep -Milliseconds 150
$sync.FirstResult = $Value
}
$secondHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "second")) -ScriptBlock {
param([string]$Value)
$sync.SecondResult = $Value
}
Assert-WinUtilAsyncHandle -Handle $firstHandle
Assert-WinUtilAsyncHandle -Handle $secondHandle
$script:sync.FirstResult | Should -Be "first"
$script:sync.SecondResult | Should -Be "second"
}
It "does not use script-scoped PowerShell or handle state" {
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1") -Raw
$runspaceScript | Should -Not -Match '\$script:powershell'
$runspaceScript | Should -Not -Match '\$script:handle'
}
}
+7 -21
View File
@@ -206,6 +206,10 @@ Describe "Compiled WinUtil sanity" {
Describe "Runspace sanity" {
BeforeAll {
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
function Write-WinUtilPerformanceCheckpoint { param($Name) }
}
It "returns a single async handle and runs a scriptblock with arguments in the shared runspace pool" {
@@ -216,44 +220,26 @@ Describe "Runspace sanity" {
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
$script:sync.runspace.Open()
$ended = $false
try {
$script:sync.Result = $null
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
param($ArgumentValue, [string]$NamedValue)
Start-Sleep -Milliseconds 200
"$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
$sync.Result = "$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
}
($handle -is [System.IAsyncResult]) | Should -BeTrue
($handle -is [array]) | Should -BeFalse
$handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
$result = $script:powershell.EndInvoke($handle)
$ended = $true
@($result)[0] | Should -Be "argument|parameter|shared"
$script:sync.Result | Should -Be "argument|parameter|shared"
} finally {
if (-not $ended -and $handle -and $handle.IsCompleted -and $script:powershell) {
try {
$script:powershell.EndInvoke($handle) | Out-Null
} catch {
# The assertion failure is more useful than cleanup errors here.
}
}
if ($script:powershell) {
$script:powershell.Dispose()
}
if ($script:sync -and $script:sync.runspace) {
$script:sync.runspace.Close()
$script:sync.runspace.Dispose()
}
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
}
}
}
+84
View File
@@ -0,0 +1,84 @@
#===========================================================================
# Tests - Toggle status checks
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilToggleStatus.ps1")
}
Describe "Get-WinUtilToggleStatus" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = @{
tweaks = [pscustomobject]@{
WPFToggleExample = [pscustomobject]@{
registry = @(
[pscustomobject]@{
Path = "HKCU:\Software\WinUtilToggle"
Name = "Enabled"
Value = "1"
OriginalValue = "0"
DefaultState = "true"
}
)
}
WPFToggleDisabledByDefault = [pscustomobject]@{
registry = @(
[pscustomobject]@{
Path = "HKCU:\Software\WinUtilToggle"
Name = "Enabled"
Value = "1"
OriginalValue = "0"
DefaultState = "false"
}
)
}
}
}
})
Mock Get-PSDrive { [pscustomobject]@{ Name = "HKU" } } -ParameterFilter { $Name -eq "HKU" }
Mock New-PSDrive { }
Mock New-Item { }
Mock Test-Path { $false }
Mock Get-ItemProperty { [pscustomobject]@{ Enabled = "1" } }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "does not create missing registry paths while reading toggle state" {
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
Should -Invoke -CommandName Test-Path -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\Software\WinUtilToggle"
}
Should -Invoke -CommandName New-Item -Times 0 -Exactly
Should -Invoke -CommandName Get-ItemProperty -Times 0 -Exactly
}
It "uses configured false default when the registry path is missing" {
Get-WinUtilToggleStatus "WPFToggleDisabledByDefault" | Should -BeFalse
Should -Invoke -CommandName New-Item -Times 0 -Exactly
}
It "caches toggle results for repeated checks" {
Mock Test-Path { $true } -ParameterFilter { $Path -eq "HKCU:\Software\WinUtilToggle" }
Mock Get-ItemProperty { [pscustomobject]@{ Enabled = "1" } } -ParameterFilter {
$Path -eq "HKCU:\Software\WinUtilToggle"
}
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
Should -Invoke -CommandName Test-Path -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\Software\WinUtilToggle"
}
Should -Invoke -CommandName Get-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\Software\WinUtilToggle"
}
}
}
+6
View File
@@ -295,13 +295,19 @@ Describe "XAML and sync wiring" {
"keys",
"ContainsKey",
"GetEnumerator",
"Remove",
"logorender",
"checkmarkrender",
"warningrender",
"InitializedTabs",
"RenderedAssetCache",
"ToggleStatusCache",
"InstallAppAreaBorder",
"InstallAppAreaScrollViewer",
"InstallAppAreaOverlay",
"InstallAppAreaOverlayText",
"InstallAppRenderQueue",
"InstallAppEntriesRendered",
"ProgressBar",
"progressBarTextBlock",
"Win11ISOImageInfo",