Defer GUI runspace pool startup

This commit is contained in:
Chris Titus
2026-07-01 22:24:01 -05:00
parent 76ce9680dc
commit 59714f81e6
10 changed files with 154 additions and 48 deletions

View File

@@ -0,0 +1,18 @@
function Close-WinUtilRunspacePool {
if ($null -eq $sync -or -not $sync.ContainsKey("runspace") -or $null -eq $sync.runspace) {
return
}
try {
if ($sync.runspace.RunspacePoolStateInfo.State -notin @(
[System.Management.Automation.Runspaces.RunspacePoolState]::Closed,
[System.Management.Automation.Runspaces.RunspacePoolState]::Closing,
[System.Management.Automation.Runspaces.RunspacePoolState]::Broken
)) {
$sync.runspace.Close()
}
} finally {
$sync.runspace.Dispose()
$sync.Remove("runspace")
}
}

View File

@@ -0,0 +1,39 @@
function Initialize-WinUtilRunspacePool {
if ($sync.runspace -and $sync.runspace.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspacePoolState]::Opened) {
return $sync.runspace
}
if ($sync.runspace) {
Close-WinUtilRunspacePool
}
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine.
$maxthreads = [Math]::Max([int]$env:NUMBER_OF_PROCESSORS, 1)
# Create a new session state for parsing variables into our runspace.
$hashVars = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'sync', $sync, $null
$offlineVar = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_OFFLINE', $PARAM_OFFLINE, $null
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$initialSessionState.Variables.Add($hashVars)
$initialSessionState.Variables.Add($offlineVar)
# Get every WinUtil/WPF function and add it to the session state.
$functions = Get-ChildItem function:\ | Where-Object { $_.Name -imatch 'winutil|WPF' }
foreach ($function in $functions) {
$functionDefinition = Get-Content function:\$($function.Name)
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $function.Name, $functionDefinition
$initialSessionState.Commands.Add($functionEntry)
}
$sync.runspace = [runspacefactory]::CreateRunspacePool(
1, # Minimum thread count
$maxthreads, # Maximum thread count
$initialSessionState, # Initial session state
$Host # Machine to create runspaces on
)
$sync.runspace.Open()
Write-WinUtilPerformanceCheckpoint -Name "Runspace pool initialized"
return $sync.runspace
}

View File

@@ -67,6 +67,8 @@ public static class WinUtilRunspaceCleanup
"@
}
Initialize-WinUtilRunspacePool | Out-Null
# Create a PowerShell instance
$powershell = [powershell]::Create()

View File

@@ -70,7 +70,8 @@ Describe "Startup performance checkpoints" {
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
$startupText = "$mainScript`n$lazyTabScript"
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1") -Raw
$startupText = "$mainScript`n$lazyTabScript`n$runspaceScript"
foreach ($checkpoint in @(
"Runspace pool initialized",

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

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,6 +20,8 @@ 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 {

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" {

View File

@@ -295,6 +295,7 @@ Describe "XAML and sync wiring" {
"keys",
"ContainsKey",
"GetEnumerator",
"Remove",
"logorender",
"checkmarkrender",
"warningrender",

View File

@@ -29,40 +29,6 @@ public enum PackageManagers
}
"@
# SPDX-License-Identifier: MIT
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine
$maxthreads = [int]$env:NUMBER_OF_PROCESSORS
# Create a new session state for parsing variables into our runspace
$hashVars = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'sync',$sync,$Null
$offlineVar = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_OFFLINE',$PARAM_OFFLINE,$Null
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
# Add the variable to the session state
$InitialSessionState.Variables.Add($hashVars)
$InitialSessionState.Variables.Add($offlineVar)
# Get every private function and add them to the session state
$functions = Get-ChildItem function:\ | Where-Object { $_.Name -imatch 'winutil|WPF' }
foreach ($function in $functions) {
$functionDefinition = Get-Content function:\$($function.name)
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $($function.name), $functionDefinition
$initialSessionState.Commands.Add($functionEntry)
}
# Create the runspace pool
$sync.runspace = [runspacefactory]::CreateRunspacePool(
1, # Minimum thread count
$maxthreads, # Maximum thread count
$InitialSessionState, # Initial session state
$Host # Machine to create runspaces on
)
# Open the RunspacePool instance
$sync.runspace.Open()
Write-WinUtilPerformanceCheckpoint -Name "Runspace pool initialized"
# Create classes for different exceptions
class WingetFailedInstall : Exception {
@@ -97,6 +63,8 @@ Set-Preferences
Write-WinUtilPerformanceCheckpoint -Name "Preferences loaded"
if ($Preset) {
Initialize-WinUtilRunspacePool | Out-Null
# Selects the tweaks from $Preset varible
Update-WinUtilSelections -flatJson $sync.configs.preset.$Preset
@@ -104,21 +72,21 @@ if ($Preset) {
Invoke-WinUtilAutoRun
# Cleanup and exit
$sync.runspace.Dispose()
$sync.runspace.Close()
Close-WinUtilRunspacePool
[System.GC]::Collect()
Stop-Transcript
return
}
if ($Config) {
Initialize-WinUtilRunspacePool | Out-Null
Invoke-WPFImpex -type "import" -Config $Config
Invoke-WinUtilAutoRun
# Cleanup and exit
$sync.runspace.Dispose()
$sync.runspace.Close()
Close-WinUtilRunspacePool
[System.GC]::Collect()
Stop-Transcript
return
@@ -148,8 +116,7 @@ try {
if (-NOT ($readerOperationSuccessful)) {
Write-Host "Failed to parse xaml content using Windows.Markup.XamlReader's Load Method." -ForegroundColor Red
Write-Host "Quitting WinUtil..." -ForegroundColor Red
$sync.runspace.Dispose()
$sync.runspace.Close()
Close-WinUtilRunspacePool
[System.GC]::Collect()
exit 1
}
@@ -255,8 +222,7 @@ $sync["Form"].title = $sync["Form"].title + " " + $sync.version
# Set the commands that will run when the form is closed
$sync["Form"].Add_Closing({
Stop-WinUtilPerformanceTrace -Name "Window closing"
$sync.runspace.Dispose()
$sync.runspace.Close()
Close-WinUtilRunspacePool
[System.GC]::Collect()
})
@@ -373,6 +339,7 @@ $sync["Form"].Add_ContentRendered({
}
$sync["Form"].Focus()
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Initialize-WinUtilRunspacePool | Out-Null }) | Out-Null
})
# The SearchBarTimer is used to delay the search operation until the user has stopped typing for a short period

View File

@@ -44,11 +44,11 @@ WinUtil currently does too much work before the first GUI paint. Work from the t
## P5 - Defer Runspace Pool Startup
- [ ] Move GUI runspace pool creation after first render when no automation mode is active.
- [ ] Keep synchronous runspace pool creation for `-Preset` and `-Config`.
- [ ] Prewarm the pool shortly after `ContentRendered` so later actions do not feel delayed.
- [ ] Ensure closing the form disposes the pool if it was created.
- [ ] Verify buttons that queue work create or reuse the pool safely.
- [x] Move GUI runspace pool creation after first render when no automation mode is active.
- [x] Keep synchronous runspace pool creation for `-Preset` and `-Config`.
- [x] Prewarm the pool shortly after `ContentRendered` so later actions do not feel delayed.
- [x] Ensure closing the form disposes the pool if it was created.
- [x] Verify buttons that queue work create or reuse the pool safely.
## P6 - Asset And Theme Cost