mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
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:
@@ -188,3 +188,4 @@ When the user corrects an agent approach, add or tighten one concrete rule here
|
|||||||
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
||||||
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
||||||
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
||||||
|
- Keep performance tracing helpers under `tools/perf`; include them in generated output only through `Compile.ps1 -Trace`.
|
||||||
|
|||||||
+40
-3
@@ -1,16 +1,46 @@
|
|||||||
param (
|
param (
|
||||||
[switch]$Run
|
[switch]$Run,
|
||||||
|
[switch]$Trace
|
||||||
)
|
)
|
||||||
|
|
||||||
$OFS = "`r`n"
|
$OFS = "`r`n"
|
||||||
|
|
||||||
|
function Get-WinUtilTraceStrippedContent {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Content
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Trace) {
|
||||||
|
return $Content
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredLines = $Content -split "`r?`n" | Where-Object {
|
||||||
|
$_ -notmatch '^\s*(Start-WinUtilPerformanceTrace|Stop-WinUtilPerformanceTrace|Write-WinUtilPerformanceCheckpoint)\b'
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredLines -join "`r`n"
|
||||||
|
}
|
||||||
|
|
||||||
# Variable to sync between runspaces
|
# Variable to sync between runspaces
|
||||||
$sync = [Hashtable]::Synchronized(@{})
|
$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')
|
||||||
|
|
||||||
$script += Get-ChildItem -Path functions -Recurse -File | Get-Content -Raw
|
$script += Get-ChildItem -Path functions -Recurse -File | ForEach-Object {
|
||||||
|
Get-WinUtilTraceStrippedContent -Content (Get-Content -Path $_.FullName -Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Trace) {
|
||||||
|
$script += Get-ChildItem -Path tools\perf -Filter *.ps1 -File | Get-Content -Raw
|
||||||
|
|
||||||
|
$script += @'
|
||||||
|
$sync.PerformanceTraceEnabled = $true
|
||||||
|
Start-WinUtilPerformanceTrace
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Config load start"
|
||||||
|
'@
|
||||||
|
}
|
||||||
|
|
||||||
Get-ChildItem config | ForEach-Object {
|
Get-ChildItem config | ForEach-Object {
|
||||||
$obj = Get-Content -Path $_.FullName -Raw | ConvertFrom-Json
|
$obj = Get-Content -Path $_.FullName -Raw | ConvertFrom-Json
|
||||||
@@ -27,6 +57,13 @@ Get-ChildItem config | ForEach-Object {
|
|||||||
|
|
||||||
$sync.configs[$_.BaseName] = $obj
|
$sync.configs[$_.BaseName] = $obj
|
||||||
$script += "`$sync.configs.$($_.BaseName) = @'`r`n$json`r`n'@ | ConvertFrom-Json"
|
$script += "`$sync.configs.$($_.BaseName) = @'`r`n$json`r`n'@ | ConvertFrom-Json"
|
||||||
|
if ($Trace) {
|
||||||
|
$script += "`r`nWrite-WinUtilPerformanceCheckpoint -Name `"Config $($_.BaseName) loaded`""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Trace) {
|
||||||
|
$script += "`r`nWrite-WinUtilPerformanceCheckpoint -Name `"Config load complete`""
|
||||||
}
|
}
|
||||||
|
|
||||||
$xaml = Get-Content -Path xaml\inputXML.xaml -Raw
|
$xaml = Get-Content -Path xaml\inputXML.xaml -Raw
|
||||||
@@ -35,7 +72,7 @@ $script += "`$inputXML = @'`r`n$xaml`r`n'@"
|
|||||||
$autounattendXml = Get-Content -Path tools\autounattend.xml -Raw
|
$autounattendXml = Get-Content -Path tools\autounattend.xml -Raw
|
||||||
$script += "`$WinUtilAutounattendXml = @'`r`n$autounattendXml`r`n'@"
|
$script += "`$WinUtilAutounattendXml = @'`r`n$autounattendXml`r`n'@"
|
||||||
|
|
||||||
$script += Get-Content -Path scripts\main.ps1 -Raw
|
$script += Get-WinUtilTraceStrippedContent -Content (Get-Content -Path scripts\main.ps1 -Raw)
|
||||||
|
|
||||||
Set-Content -Path winutil.ps1 -Value $script
|
Set-Content -Path winutil.ps1 -Value $script
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,29 +2,39 @@ Function Get-WinUtilToggleStatus ($ToggleSwitch) {
|
|||||||
|
|
||||||
$ToggleSwitchReg = $sync.configs.tweaks.$ToggleSwitch.registry
|
$ToggleSwitchReg = $sync.configs.tweaks.$ToggleSwitch.registry
|
||||||
|
|
||||||
|
if ($null -eq $sync.ToggleStatusCache) {
|
||||||
|
$sync.ToggleStatusCache = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.ToggleStatusCache.ContainsKey($ToggleSwitch)) {
|
||||||
|
return [bool]$sync.ToggleStatusCache[$ToggleSwitch]
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
||||||
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null
|
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($regentry in $ToggleSwitchReg) {
|
foreach ($regentry in $ToggleSwitchReg) {
|
||||||
|
|
||||||
if (-not (Test-Path $regentry.Path)) {
|
if (Test-Path $regentry.Path) {
|
||||||
New-Item -Path $regentry.Path -Force | Out-Null
|
$regstate = (Get-ItemProperty -Path $regentry.Path).$($regentry.Name)
|
||||||
|
} else {
|
||||||
|
$regstate = $null
|
||||||
}
|
}
|
||||||
|
|
||||||
$regstate = (Get-ItemProperty -Path $regentry.Path).$($regentry.Name)
|
|
||||||
|
|
||||||
if ($null -eq $regstate) {
|
if ($null -eq $regstate) {
|
||||||
switch ($regentry.DefaultState) {
|
switch ([string]$regentry.DefaultState) {
|
||||||
"true" { $regstate = $regentry.Value }
|
"true" { $regstate = $regentry.Value }
|
||||||
"false" { $regstate = $regentry.OriginalValue }
|
"false" { $regstate = $regentry.OriginalValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($regstate -ne $regentry.Value) {
|
if ($regstate -ne $regentry.Value) {
|
||||||
|
$sync.ToggleStatusCache[$ToggleSwitch] = $false
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sync.ToggleStatusCache[$ToggleSwitch] = $true
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ function Initialize-InstallAppEntry {
|
|||||||
$appKey
|
$appKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$app = $sync.configs.applicationsHashtable.$appKey
|
||||||
|
|
||||||
# Create the outer Border for the application type
|
# Create the outer Border for the application type
|
||||||
$border = New-Object Windows.Controls.Border
|
$border = New-Object Windows.Controls.Border
|
||||||
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
|
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
|
||||||
$border.Tag = $appKey
|
$border.Tag = $appKey
|
||||||
$border.ToolTip = $Apps.$appKey.description
|
$border.ToolTip = $app.description
|
||||||
$border.Add_MouseLeftButtonUp({
|
$border.Add_MouseLeftButtonUp({
|
||||||
$childCheckbox = ($this.Child | Where-Object {$_.Template.TargetType -eq [System.Windows.Controls.Checkbox]})[0]
|
$childCheckbox = ($this.Child | Where-Object {$_.Template.TargetType -eq [System.Windows.Controls.Checkbox]})[0]
|
||||||
$childCheckBox.isChecked = -not $childCheckbox.IsChecked
|
$childCheckBox.isChecked = -not $childCheckbox.IsChecked
|
||||||
@@ -61,10 +63,10 @@ function Initialize-InstallAppEntry {
|
|||||||
# Create the TextBlock for the application name
|
# Create the TextBlock for the application name
|
||||||
$appName = New-Object Windows.Controls.TextBlock
|
$appName = New-Object Windows.Controls.TextBlock
|
||||||
$appName.Style = $sync.Form.Resources.AppEntryNameStyle
|
$appName.Style = $sync.Form.Resources.AppEntryNameStyle
|
||||||
$appName.Text = $Apps.$appKey.content
|
$appName.Text = $app.content
|
||||||
|
|
||||||
# Add FOSS label after the name if FOSS
|
# Add FOSS label after the name if FOSS
|
||||||
if ($Apps.$appKey.foss -eq $true) {
|
if ($app.foss -eq $true) {
|
||||||
$fossRun = [System.Windows.Documents.Run]::new(" $([char]0x25CF)")
|
$fossRun = [System.Windows.Documents.Run]::new(" $([char]0x25CF)")
|
||||||
$fossRun.Foreground = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(110, 255, 114))
|
$fossRun.Foreground = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(110, 255, 114))
|
||||||
$fossRun.FontSize = 11.5
|
$fossRun.FontSize = 11.5
|
||||||
@@ -74,10 +76,13 @@ function Initialize-InstallAppEntry {
|
|||||||
$checkBox.Content = $appName
|
$checkBox.Content = $appName
|
||||||
|
|
||||||
# Add accessibility properties to make the elements screen reader friendly
|
# Add accessibility properties to make the elements screen reader friendly
|
||||||
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $Apps.$appKey.content)
|
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||||
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $Apps.$appKey.content)
|
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||||
|
|
||||||
$border.Child = $checkBox
|
$border.Child = $checkBox
|
||||||
|
if ($sync.selectedApps -contains $appKey) {
|
||||||
|
$checkBox.IsChecked = $true
|
||||||
|
}
|
||||||
# Add the border to the corresponding Category
|
# Add the border to the corresponding Category
|
||||||
$TargetElement.Children.Add($border) | Out-Null
|
$TargetElement.Children.Add($border) | Out-Null
|
||||||
return $checkbox
|
return $checkbox
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function Initialize-InstallCategoryAppList {
|
|||||||
$Apps
|
$Apps
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-group apps by category
|
# Pre-group apps by category before creating WPF controls.
|
||||||
$appsByCategory = @{}
|
$appsByCategory = @{}
|
||||||
foreach ($appKey in $Apps.Keys) {
|
foreach ($appKey in $Apps.Keys) {
|
||||||
$category = $Apps.$appKey.Category
|
$category = $Apps.$appKey.Category
|
||||||
@@ -25,6 +25,8 @@ function Initialize-InstallCategoryAppList {
|
|||||||
}
|
}
|
||||||
$appsByCategory[$category] += $appKey
|
$appsByCategory[$category] += $appKey
|
||||||
}
|
}
|
||||||
|
$sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
|
||||||
|
|
||||||
foreach ($category in $($appsByCategory.Keys | Sort-Object)) {
|
foreach ($category in $($appsByCategory.Keys | Sort-Object)) {
|
||||||
# Create a container for category label + apps
|
# Create a container for category label + apps
|
||||||
$categoryContainer = New-Object Windows.Controls.StackPanel
|
$categoryContainer = New-Object Windows.Controls.StackPanel
|
||||||
@@ -52,10 +54,10 @@ function Initialize-InstallCategoryAppList {
|
|||||||
|
|
||||||
# Add click handler to toggle category visibility
|
# Add click handler to toggle category visibility
|
||||||
$toggleButton.Add_MouseLeftButtonUp({
|
$toggleButton.Add_MouseLeftButtonUp({
|
||||||
param($sender, $e)
|
param($categoryToggle)
|
||||||
|
|
||||||
# Find the parent StackPanel (categoryContainer)
|
# Find the parent StackPanel (categoryContainer)
|
||||||
$categoryContainer = $sender.Parent
|
$categoryContainer = $categoryToggle.Parent
|
||||||
if ($categoryContainer -and $categoryContainer.Children.Count -ge 2) {
|
if ($categoryContainer -and $categoryContainer.Children.Count -ge 2) {
|
||||||
# The WrapPanel is the second child
|
# The WrapPanel is the second child
|
||||||
$wrapPanel = $categoryContainer.Children[1]
|
$wrapPanel = $categoryContainer.Children[1]
|
||||||
@@ -64,11 +66,11 @@ function Initialize-InstallCategoryAppList {
|
|||||||
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
|
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
|
||||||
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
||||||
# Change - to +
|
# Change - to +
|
||||||
$sender.Content = $sender.Content -replace "^- ", "+ "
|
$categoryToggle.Content = $categoryToggle.Content -replace "^- ", "+ "
|
||||||
} else {
|
} else {
|
||||||
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
||||||
# Change + to -
|
# Change + to -
|
||||||
$sender.Content = $sender.Content -replace "^\+ ", "- "
|
$categoryToggle.Content = $categoryToggle.Content -replace "^\+ ", "- "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -89,9 +91,12 @@ function Initialize-InstallCategoryAppList {
|
|||||||
# Add the entire category container to the target element
|
# Add the entire category container to the target element
|
||||||
$null = $TargetElement.Items.Add($categoryContainer)
|
$null = $TargetElement.Items.Add($categoryContainer)
|
||||||
|
|
||||||
# Add apps to the wrap panel
|
$sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{
|
||||||
$appsByCategory[$category] | Sort-Object | ForEach-Object {
|
Category = $category
|
||||||
$sync.$_ = $(Initialize-InstallAppEntry -TargetElement $wrapPanel -AppKey $_)
|
TargetElement = $wrapPanel
|
||||||
}
|
AppKeys = @($appsByCategory[$category] | Sort-Object)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Start-WinUtilInstallAppRendering
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
function Initialize-WinUtilTabContent {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$TabName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $sync.InitializedTabs) {
|
||||||
|
$sync.InitializedTabs = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.InitializedTabs[$TabName]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($TabName) {
|
||||||
|
"Install" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
|
||||||
|
Initialize-WPFUI -targetGridName "appscategory"
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "App navigation UI created"
|
||||||
|
|
||||||
|
Initialize-WPFUI -targetGridName "appspanel"
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Install UI created"
|
||||||
|
}
|
||||||
|
"Tweaks" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.tweaks -targetGridName "tweakspanel" -columncount 2
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Tweaks UI created"
|
||||||
|
}
|
||||||
|
"Config" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.feature -targetGridName "featurespanel" -columncount 2
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Features UI created"
|
||||||
|
}
|
||||||
|
"AppX" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "AppX UI created"
|
||||||
|
}
|
||||||
|
"Win11 Creator" {
|
||||||
|
if ($sync.Form -and $sync.Form.Dispatcher) {
|
||||||
|
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
||||||
|
}
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Win11 ISO tab initialized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sync.InitializedTabs[$TabName] = $true
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
function Initialize-WinUtilTaskbarOverlayAssets {
|
||||||
|
param(
|
||||||
|
[bool]$IncludeLogo = $true,
|
||||||
|
[bool]$IncludeStatusAssets = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($IncludeLogo -and -not $sync["logorender"]) {
|
||||||
|
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Taskbar logo asset rendered"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeStatusAssets -and -not $sync["checkmarkrender"]) {
|
||||||
|
$sync["checkmarkrender"] = (Invoke-WinUtilAssets -Type "checkmark" -Size 512 -Render)
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Taskbar checkmark asset rendered"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeStatusAssets -and -not $sync["warningrender"]) {
|
||||||
|
$sync["warningrender"] = (Invoke-WinUtilAssets -Type "warning" -Size 512 -Render)
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Taskbar warning asset rendered"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,17 @@ function Invoke-WinUtilAssets {
|
|||||||
[switch]$render
|
[switch]$render
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ($render -and $null -ne $sync) {
|
||||||
|
if ($null -eq $sync.RenderedAssetCache) {
|
||||||
|
$sync.RenderedAssetCache = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = "$(([string]$type).ToLowerInvariant())|$Size"
|
||||||
|
if ($sync.RenderedAssetCache.ContainsKey($cacheKey)) {
|
||||||
|
return $sync.RenderedAssetCache[$cacheKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Create the Viewbox and set its size
|
# Create the Viewbox and set its size
|
||||||
$LogoViewbox = New-Object Windows.Controls.Viewbox
|
$LogoViewbox = New-Object Windows.Controls.Viewbox
|
||||||
$LogoViewbox.Width = $Size
|
$LogoViewbox.Width = $Size
|
||||||
@@ -191,6 +202,13 @@ C 21.36,47.14 28.67,50.71 30.01,52.63
|
|||||||
$bitmapImage.StreamSource = $imageStream
|
$bitmapImage.StreamSource = $imageStream
|
||||||
$bitmapImage.CacheOption = [Windows.Media.Imaging.BitmapCacheOption]::OnLoad
|
$bitmapImage.CacheOption = [Windows.Media.Imaging.BitmapCacheOption]::OnLoad
|
||||||
$bitmapImage.EndInit()
|
$bitmapImage.EndInit()
|
||||||
|
if ($bitmapImage.CanFreeze) {
|
||||||
|
$bitmapImage.Freeze()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $sync -and $sync.ContainsKey("RenderedAssetCache")) {
|
||||||
|
$sync.RenderedAssetCache[$cacheKey] = $bitmapImage
|
||||||
|
}
|
||||||
|
|
||||||
return $bitmapImage
|
return $bitmapImage
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -61,12 +61,21 @@ function Set-WinUtilTaskbaritem {
|
|||||||
if ($overlay) {
|
if ($overlay) {
|
||||||
switch ($overlay) {
|
switch ($overlay) {
|
||||||
'logo' {
|
'logo' {
|
||||||
|
if (-not $sync["logorender"]) {
|
||||||
|
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $true -IncludeStatusAssets $false
|
||||||
|
}
|
||||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["logorender"]
|
$sync["Form"].taskbarItemInfo.Overlay = $sync["logorender"]
|
||||||
}
|
}
|
||||||
'checkmark' {
|
'checkmark' {
|
||||||
|
if (-not $sync["checkmarkrender"]) {
|
||||||
|
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $false -IncludeStatusAssets $true
|
||||||
|
}
|
||||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["checkmarkrender"]
|
$sync["Form"].taskbarItemInfo.Overlay = $sync["checkmarkrender"]
|
||||||
}
|
}
|
||||||
'warning' {
|
'warning' {
|
||||||
|
if (-not $sync["warningrender"]) {
|
||||||
|
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $false -IncludeStatusAssets $true
|
||||||
|
}
|
||||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["warningrender"]
|
$sync["Form"].taskbarItemInfo.Overlay = $sync["warningrender"]
|
||||||
}
|
}
|
||||||
'None' {
|
'None' {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
function Invoke-WinUtilInstallAppRenderBatch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$CategoryBatch
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($appKey in $CategoryBatch.AppKeys) {
|
||||||
|
$sync.$appKey = Initialize-InstallAppEntry -TargetElement $CategoryBatch.TargetElement -AppKey $appKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.currentTab -eq "Install" -and $sync.SearchBar -and -not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text)) {
|
||||||
|
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Complete-WinUtilInstallAppRendering {
|
||||||
|
$sync.InstallAppEntriesRendered = $true
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Install app entries rendered"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WinUtilInstallAppRenderNextBatch {
|
||||||
|
if ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||||
|
$categoryBatch = $sync.InstallAppRenderQueue.Dequeue()
|
||||||
|
Invoke-WinUtilInstallAppRenderBatch -CategoryBatch $categoryBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||||
|
$sync.Form.Dispatcher.BeginInvoke(
|
||||||
|
[System.Windows.Threading.DispatcherPriority]::Background,
|
||||||
|
[action]{ Invoke-WinUtilInstallAppRenderNextBatch }
|
||||||
|
) | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Complete-WinUtilInstallAppRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-WinUtilInstallAppRendering {
|
||||||
|
if ($null -eq $sync.InstallAppRenderQueue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$sync.InstallAppEntriesRendered = $false
|
||||||
|
|
||||||
|
if ($sync.Form -and $sync.Form.Dispatcher) {
|
||||||
|
$sync.Form.Dispatcher.BeginInvoke(
|
||||||
|
[System.Windows.Threading.DispatcherPriority]::Background,
|
||||||
|
[action]{ Invoke-WinUtilInstallAppRenderNextBatch }
|
||||||
|
) | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||||
|
$categoryBatch = $sync.InstallAppRenderQueue.Dequeue()
|
||||||
|
Invoke-WinUtilInstallAppRenderBatch -CategoryBatch $categoryBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
Complete-WinUtilInstallAppRendering
|
||||||
|
}
|
||||||
@@ -30,30 +30,67 @@ function Invoke-WPFRunspace {
|
|||||||
$ParameterList
|
$ParameterList
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (-not ("WinUtilRunspaceCleanup" -as [type])) {
|
||||||
|
Add-Type @"
|
||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
|
||||||
|
public sealed class WinUtilRunspaceCleanupState
|
||||||
|
{
|
||||||
|
public PowerShell PowerShell { get; set; }
|
||||||
|
public IAsyncResult Handle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WinUtilRunspaceCleanup
|
||||||
|
{
|
||||||
|
public static void Cleanup(object state, bool timedOut)
|
||||||
|
{
|
||||||
|
var cleanupState = state as WinUtilRunspaceCleanupState;
|
||||||
|
if (cleanupState == null || cleanupState.PowerShell == null || cleanupState.Handle == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cleanupState.PowerShell.EndInvoke(cleanupState.Handle);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cleanupState.PowerShell.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize-WinUtilRunspacePool | Out-Null
|
||||||
|
|
||||||
# Create a PowerShell instance
|
# Create a PowerShell instance
|
||||||
$script:powershell = [powershell]::Create()
|
$powershell = [powershell]::Create()
|
||||||
|
|
||||||
# Add Scriptblock and Arguments to runspace
|
# Add Scriptblock and Arguments to runspace
|
||||||
[void]$script:powershell.AddScript($ScriptBlock)
|
[void]$powershell.AddScript($ScriptBlock)
|
||||||
[void]$script:powershell.AddArgument($ArgumentList)
|
[void]$powershell.AddArgument($ArgumentList)
|
||||||
|
|
||||||
foreach ($parameter in $ParameterList) {
|
foreach ($parameter in $ParameterList) {
|
||||||
[void]$script:powershell.AddParameter($parameter[0], $parameter[1])
|
[void]$powershell.AddParameter($parameter[0], $parameter[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:powershell.RunspacePool = $sync.runspace
|
$powershell.RunspacePool = $sync.runspace
|
||||||
|
|
||||||
# Execute the RunspacePool
|
# Execute the RunspacePool
|
||||||
$script:handle = $script:powershell.BeginInvoke()
|
$handle = $powershell.BeginInvoke()
|
||||||
|
|
||||||
|
$cleanupState = [WinUtilRunspaceCleanupState]::new()
|
||||||
|
$cleanupState.PowerShell = $powershell
|
||||||
|
$cleanupState.Handle = $handle
|
||||||
|
$cleanupCallback = [System.Threading.WaitOrTimerCallback][WinUtilRunspaceCleanup]::Cleanup
|
||||||
|
[System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, $cleanupCallback, $cleanupState, -1, $true) | Out-Null
|
||||||
|
|
||||||
# Clean up the RunspacePool threads when they are complete, and invoke the garbage collector to clean up the memory
|
|
||||||
if ($script:handle.IsCompleted) {
|
|
||||||
$script:powershell.EndInvoke($script:handle)
|
|
||||||
$script:powershell.Dispose()
|
|
||||||
$sync.runspace.Dispose()
|
|
||||||
$sync.runspace.Close()
|
|
||||||
[System.GC]::Collect()
|
|
||||||
}
|
|
||||||
# Return the handle
|
# Return the handle
|
||||||
return $handle
|
return $handle
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function Invoke-WPFTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
|
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
|
||||||
|
Initialize-WinUtilTabContent -TabName $sync.currentTab
|
||||||
|
|
||||||
# Always reset the filter for the current tab
|
# Always reset the filter for the current tab
|
||||||
if ($sync.currentTab -eq "Install") {
|
if ($sync.currentTab -eq "Install") {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
An error has occurred that was not properly handled. Additional information is shown below. The PowerShell process will exit.
|
||||||
|
Unhandled exception. System.ComponentModel.Win32Exception (1816): Not enough quota is available to process this command.
|
||||||
|
at System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget, Nullable`1 channelSet)
|
||||||
|
at System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr lParam)
|
||||||
|
at System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)
|
||||||
|
at System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
|
||||||
|
at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
|
||||||
|
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
|
||||||
|
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
|
||||||
|
at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
|
||||||
|
|
||||||
|
[process exited with code 3221226525 (0xc000041d)]
|
||||||
|
You can now close this terminal with Ctrl+D, or press Enter to restart.
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -245,13 +245,14 @@ Describe "Preset config" {
|
|||||||
Describe "App navigation config" {
|
Describe "App navigation config" {
|
||||||
It "is wired to an existing XAML target grid" {
|
It "is wired to an existing XAML target grid" {
|
||||||
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
||||||
|
$tabInitializerScript = Get-Content -Path (Join-Path $script:repoRoot "functions/private/Initialize-WinUtilTabContent.ps1") -Raw
|
||||||
$targetGridMatch = [regex]::Match(
|
$targetGridMatch = [regex]::Match(
|
||||||
$mainScript,
|
"$mainScript`n$tabInitializerScript",
|
||||||
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
|
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (-not $targetGridMatch.Success) {
|
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
|
$xamlText = Get-Content -Path $script:xamlPath -Raw
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
$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-WPFRunspace.ps1")
|
||||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
|
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
|
||||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
||||||
@@ -18,21 +20,17 @@ BeforeAll {
|
|||||||
$initialSessionState.Variables.Add($syncVariable)
|
$initialSessionState.Variables.Add($syncVariable)
|
||||||
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
||||||
$script:sync.runspace.Open()
|
$script:sync.runspace.Open()
|
||||||
|
|
||||||
|
function Write-WinUtilPerformanceCheckpoint { param($Name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
function script:Clear-WinUtilRunspaceTestContext {
|
function script:Clear-WinUtilRunspaceTestContext {
|
||||||
if ($script:powershell) {
|
|
||||||
$script:powershell.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($script:sync -and $script:sync.runspace) {
|
if ($script:sync -and $script:sync.runspace) {
|
||||||
$script:sync.runspace.Close()
|
$script:sync.runspace.Close()
|
||||||
$script:sync.runspace.Dispose()
|
$script:sync.runspace.Dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
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 {
|
function script:Assert-WinUtilAsyncHandle {
|
||||||
@@ -54,28 +52,34 @@ Describe "Invoke-WPFRunspace behavior" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
It "returns a single async handle with no argument list" {
|
It "returns a single async handle with no argument list" {
|
||||||
|
$script:sync.Result = $null
|
||||||
|
|
||||||
$handle = Invoke-WPFRunspace -ScriptBlock {
|
$handle = Invoke-WPFRunspace -ScriptBlock {
|
||||||
Start-Sleep -Milliseconds 100
|
Start-Sleep -Milliseconds 100
|
||||||
"no-args|$($sync.Marker)"
|
$sync.Result = "no-args|$($sync.Marker)"
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert-WinUtilAsyncHandle -Handle $handle
|
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" {
|
It "passes one named parameter" {
|
||||||
|
$script:sync.Result = $null
|
||||||
|
|
||||||
$handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
|
$handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
|
||||||
param([string]$Name)
|
param([string]$Name)
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 100
|
Start-Sleep -Milliseconds 100
|
||||||
"Name=$Name"
|
$sync.Result = "Name=$Name"
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert-WinUtilAsyncHandle -Handle $handle
|
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" {
|
It "passes multiple named parameters" {
|
||||||
|
$script:sync.Result = $null
|
||||||
|
|
||||||
$handle = Invoke-WPFRunspace -ParameterList @(
|
$handle = Invoke-WPFRunspace -ParameterList @(
|
||||||
("First", "alpha"),
|
("First", "alpha"),
|
||||||
("Second", "beta")
|
("Second", "beta")
|
||||||
@@ -86,21 +90,57 @@ Describe "Invoke-WPFRunspace behavior" {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 100
|
Start-Sleep -Milliseconds 100
|
||||||
"$First|$Second|$($sync.Marker)"
|
$sync.Result = "$First|$Second|$($sync.Marker)"
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert-WinUtilAsyncHandle -Handle $handle
|
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 {
|
$handle = Invoke-WPFRunspace -ScriptBlock {
|
||||||
Start-Sleep -Milliseconds 100
|
Start-Sleep -Milliseconds 100
|
||||||
throw "runspace failure"
|
throw "runspace failure"
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert-WinUtilAsyncHandle -Handle $handle
|
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
@@ -206,6 +206,10 @@ Describe "Compiled WinUtil sanity" {
|
|||||||
Describe "Runspace sanity" {
|
Describe "Runspace sanity" {
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
|
. (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" {
|
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 = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
||||||
$script:sync.runspace.Open()
|
$script:sync.runspace.Open()
|
||||||
|
|
||||||
$ended = $false
|
|
||||||
try {
|
try {
|
||||||
|
$script:sync.Result = $null
|
||||||
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
|
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
|
||||||
param($ArgumentValue, [string]$NamedValue)
|
param($ArgumentValue, [string]$NamedValue)
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 200
|
Start-Sleep -Milliseconds 200
|
||||||
"$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
|
$sync.Result = "$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
|
||||||
}
|
}
|
||||||
|
|
||||||
($handle -is [System.IAsyncResult]) | Should -BeTrue
|
($handle -is [System.IAsyncResult]) | Should -BeTrue
|
||||||
($handle -is [array]) | Should -BeFalse
|
($handle -is [array]) | Should -BeFalse
|
||||||
$handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
|
$handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
|
||||||
|
$script:sync.Result | Should -Be "argument|parameter|shared"
|
||||||
$result = $script:powershell.EndInvoke($handle)
|
|
||||||
$ended = $true
|
|
||||||
|
|
||||||
@($result)[0] | Should -Be "argument|parameter|shared"
|
|
||||||
} finally {
|
} 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) {
|
if ($script:sync -and $script:sync.runspace) {
|
||||||
$script:sync.runspace.Close()
|
$script:sync.runspace.Close()
|
||||||
$script:sync.runspace.Dispose()
|
$script:sync.runspace.Dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||||
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
|
|
||||||
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -295,13 +295,19 @@ Describe "XAML and sync wiring" {
|
|||||||
"keys",
|
"keys",
|
||||||
"ContainsKey",
|
"ContainsKey",
|
||||||
"GetEnumerator",
|
"GetEnumerator",
|
||||||
|
"Remove",
|
||||||
"logorender",
|
"logorender",
|
||||||
"checkmarkrender",
|
"checkmarkrender",
|
||||||
"warningrender",
|
"warningrender",
|
||||||
|
"InitializedTabs",
|
||||||
|
"RenderedAssetCache",
|
||||||
|
"ToggleStatusCache",
|
||||||
"InstallAppAreaBorder",
|
"InstallAppAreaBorder",
|
||||||
"InstallAppAreaScrollViewer",
|
"InstallAppAreaScrollViewer",
|
||||||
"InstallAppAreaOverlay",
|
"InstallAppAreaOverlay",
|
||||||
"InstallAppAreaOverlayText",
|
"InstallAppAreaOverlayText",
|
||||||
|
"InstallAppRenderQueue",
|
||||||
|
"InstallAppEntriesRendered",
|
||||||
"ProgressBar",
|
"ProgressBar",
|
||||||
"progressBarTextBlock",
|
"progressBarTextBlock",
|
||||||
"Win11ISOImageInfo",
|
"Win11ISOImageInfo",
|
||||||
|
|||||||
+20
-60
@@ -29,39 +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()
|
|
||||||
|
|
||||||
# Create classes for different exceptions
|
# Create classes for different exceptions
|
||||||
|
|
||||||
class WingetFailedInstall : Exception {
|
class WingetFailedInstall : Exception {
|
||||||
@@ -90,10 +57,14 @@ $sync.configs.appxHashtable = @{}
|
|||||||
$sync.configs.appx.PSObject.Properties | ForEach-Object {
|
$sync.configs.appx.PSObject.Properties | ForEach-Object {
|
||||||
$sync.configs.appxHashtable[$_.Name] = $_.Value
|
$sync.configs.appxHashtable[$_.Name] = $_.Value
|
||||||
}
|
}
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Config hashtables initialized"
|
||||||
|
|
||||||
Set-Preferences
|
Set-Preferences
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Preferences loaded"
|
||||||
|
|
||||||
if ($Preset) {
|
if ($Preset) {
|
||||||
|
Initialize-WinUtilRunspacePool | Out-Null
|
||||||
|
|
||||||
# Selects the tweaks from $Preset varible
|
# Selects the tweaks from $Preset varible
|
||||||
Update-WinUtilSelections -flatJson $sync.configs.preset.$Preset
|
Update-WinUtilSelections -flatJson $sync.configs.preset.$Preset
|
||||||
|
|
||||||
@@ -101,21 +72,21 @@ if ($Preset) {
|
|||||||
Invoke-WinUtilAutoRun
|
Invoke-WinUtilAutoRun
|
||||||
|
|
||||||
# Cleanup and exit
|
# Cleanup and exit
|
||||||
$sync.runspace.Dispose()
|
Close-WinUtilRunspacePool
|
||||||
$sync.runspace.Close()
|
|
||||||
[System.GC]::Collect()
|
[System.GC]::Collect()
|
||||||
Stop-Transcript
|
Stop-Transcript
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Config) {
|
if ($Config) {
|
||||||
|
Initialize-WinUtilRunspacePool | Out-Null
|
||||||
|
|
||||||
Invoke-WPFImpex -type "import" -Config $Config
|
Invoke-WPFImpex -type "import" -Config $Config
|
||||||
|
|
||||||
Invoke-WinUtilAutoRun
|
Invoke-WinUtilAutoRun
|
||||||
|
|
||||||
# Cleanup and exit
|
# Cleanup and exit
|
||||||
$sync.runspace.Dispose()
|
Close-WinUtilRunspacePool
|
||||||
$sync.runspace.Close()
|
|
||||||
[System.GC]::Collect()
|
[System.GC]::Collect()
|
||||||
Stop-Transcript
|
Stop-Transcript
|
||||||
return
|
return
|
||||||
@@ -130,6 +101,7 @@ $reader = (New-Object System.Xml.XmlNodeReader $xaml)
|
|||||||
try {
|
try {
|
||||||
$sync["Form"] = [Windows.Markup.XamlReader]::Load( $reader )
|
$sync["Form"] = [Windows.Markup.XamlReader]::Load( $reader )
|
||||||
$readerOperationSuccessful = $true
|
$readerOperationSuccessful = $true
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "XAML loaded"
|
||||||
} catch [System.Management.Automation.MethodInvocationException] {
|
} catch [System.Management.Automation.MethodInvocationException] {
|
||||||
Write-Host "We ran into a problem with the XAML code. Check the syntax for this control..." -ForegroundColor Red
|
Write-Host "We ran into a problem with the XAML code. Check the syntax for this control..." -ForegroundColor Red
|
||||||
Write-Host $error[0].Exception.Message -ForegroundColor Red
|
Write-Host $error[0].Exception.Message -ForegroundColor Red
|
||||||
@@ -144,8 +116,7 @@ try {
|
|||||||
if (-NOT ($readerOperationSuccessful)) {
|
if (-NOT ($readerOperationSuccessful)) {
|
||||||
Write-Host "Failed to parse xaml content using Windows.Markup.XamlReader's Load Method." -ForegroundColor Red
|
Write-Host "Failed to parse xaml content using Windows.Markup.XamlReader's Load Method." -ForegroundColor Red
|
||||||
Write-Host "Quitting WinUtil..." -ForegroundColor Red
|
Write-Host "Quitting WinUtil..." -ForegroundColor Red
|
||||||
$sync.runspace.Dispose()
|
Close-WinUtilRunspacePool
|
||||||
$sync.runspace.Close()
|
|
||||||
[System.GC]::Collect()
|
[System.GC]::Collect()
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -179,19 +150,12 @@ $sync.Form.Add_Loaded({
|
|||||||
})
|
})
|
||||||
|
|
||||||
Invoke-WinutilThemeChange -theme $sync.preferences.theme
|
Invoke-WinutilThemeChange -theme $sync.preferences.theme
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Theme applied"
|
||||||
|
|
||||||
|
|
||||||
# Now call the function with the final merged config
|
# Build only the default tab before first paint; other tabs initialize on first activation.
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
|
$sync.InitializedTabs = @{}
|
||||||
Initialize-WPFUI -targetGridName "appscategory"
|
Initialize-WinUtilTabContent -TabName "Install"
|
||||||
|
|
||||||
Initialize-WPFUI -targetGridName "appspanel"
|
|
||||||
|
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.tweaks -targetGridName "tweakspanel" -columncount 2
|
|
||||||
|
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.feature -targetGridName "featurespanel" -columncount 2
|
|
||||||
|
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2
|
|
||||||
|
|
||||||
# Future implementation: Add Windows Version to updates panel
|
# Future implementation: Add Windows Version to updates panel
|
||||||
#Invoke-WPFUIElements -configVariable $sync.configs.updates -targetGridName "updatespanel" -columncount 1
|
#Invoke-WPFUIElements -configVariable $sync.configs.updates -targetGridName "updatespanel" -columncount 1
|
||||||
@@ -257,8 +221,8 @@ Set-WinUtilTaskbaritem -state "None"
|
|||||||
$sync["Form"].title = $sync["Form"].title + " " + $sync.version
|
$sync["Form"].title = $sync["Form"].title + " " + $sync.version
|
||||||
# Set the commands that will run when the form is closed
|
# Set the commands that will run when the form is closed
|
||||||
$sync["Form"].Add_Closing({
|
$sync["Form"].Add_Closing({
|
||||||
$sync.runspace.Dispose()
|
Stop-WinUtilPerformanceTrace -Name "Window closing"
|
||||||
$sync.runspace.Close()
|
Close-WinUtilRunspacePool
|
||||||
[System.GC]::Collect()
|
[System.GC]::Collect()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -326,6 +290,7 @@ $sync["Form"].Add_Deactivated({
|
|||||||
})
|
})
|
||||||
|
|
||||||
$sync["Form"].Add_ContentRendered({
|
$sync["Form"].Add_ContentRendered({
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "First content rendered"
|
||||||
# Load the Windows Forms assembly
|
# Load the Windows Forms assembly
|
||||||
Add-Type -AssemblyName System.Windows.Forms
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
$primaryScreen = [System.Windows.Forms.Screen]::PrimaryScreen
|
$primaryScreen = [System.Windows.Forms.Screen]::PrimaryScreen
|
||||||
@@ -374,6 +339,8 @@ $sync["Form"].Add_ContentRendered({
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sync["Form"].Focus()
|
$sync["Form"].Focus()
|
||||||
|
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Initialize-WinUtilRunspacePool | Out-Null }) | Out-Null
|
||||||
|
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $false -IncludeStatusAssets $true }) | Out-Null
|
||||||
})
|
})
|
||||||
|
|
||||||
# The SearchBarTimer is used to delay the search operation until the user has stopped typing for a short period
|
# The SearchBarTimer is used to delay the search operation until the user has stopped typing for a short period
|
||||||
@@ -418,10 +385,7 @@ $sync["Form"].Add_Loaded({
|
|||||||
|
|
||||||
$NavLogoPanel = $sync["Form"].FindName("NavLogoPanel")
|
$NavLogoPanel = $sync["Form"].FindName("NavLogoPanel")
|
||||||
$NavLogoPanel.Children.Add((Invoke-WinUtilAssets -Type "logo" -Size 25)) | Out-Null
|
$NavLogoPanel.Children.Add((Invoke-WinUtilAssets -Type "logo" -Size 25)) | Out-Null
|
||||||
|
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $true -IncludeStatusAssets $false
|
||||||
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
|
|
||||||
$sync["checkmarkrender"] = (Invoke-WinUtilAssets -Type "checkmark" -Size 512 -Render)
|
|
||||||
$sync["warningrender"] = (Invoke-WinUtilAssets -Type "warning" -Size 512 -Render)
|
|
||||||
|
|
||||||
Set-WinUtilTaskbaritem -overlay "logo"
|
Set-WinUtilTaskbaritem -overlay "logo"
|
||||||
|
|
||||||
@@ -514,10 +478,6 @@ $sync["FontScalingApplyButton"].Add_Click({
|
|||||||
|
|
||||||
# ── Win11ISO Tab button handlers ──────────────────────────────────────────────
|
# ── Win11ISO Tab button handlers ──────────────────────────────────────────────
|
||||||
|
|
||||||
$sync["WPFTab5BT"].Add_Click({
|
|
||||||
$sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
|
||||||
})
|
|
||||||
|
|
||||||
$sync["WPFWin11ISOBrowseButton"].Add_Click({
|
$sync["WPFWin11ISOBrowseButton"].Add_Click({
|
||||||
Invoke-WinUtilISOBrowse
|
Invoke-WinUtilISOBrowse
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Speed TODO
|
||||||
|
|
||||||
|
WinUtil currently does too much work before the first GUI paint. Work from the top down: measure first, improve perceived launch time next, then clean up deeper execution/runspace paths.
|
||||||
|
|
||||||
|
## P0 - Measure First
|
||||||
|
|
||||||
|
- [x] Add temporary startup timing checkpoints with `System.Diagnostics.Stopwatch`.
|
||||||
|
- [x] Measure config embedding/load, runspace pool initialization, XAML load, theme application, install UI creation, tweaks UI creation, feature UI creation, AppX UI creation, and asset rendering.
|
||||||
|
- [x] Record baseline launch timing on a clean run and a warm run.
|
||||||
|
- [x] Keep timing output in the existing WinUtil log/transcript path.
|
||||||
|
- [x] Remove or gate temporary timing noise after the optimization work is validated.
|
||||||
|
|
||||||
|
## P1 - Faster First Paint
|
||||||
|
|
||||||
|
- [x] Show the main window before building every non-visible panel.
|
||||||
|
- [x] Build only the default visible tab during startup.
|
||||||
|
- [x] Move Tweaks, Features, AppX, and Win11 ISO tab initialization to first tab activation.
|
||||||
|
- [x] Add one-time guards so each lazy tab initializes only once.
|
||||||
|
- [x] Preserve current behavior for `-Preset` and `-Config` automation paths.
|
||||||
|
|
||||||
|
## P2 - Install Tab Rendering
|
||||||
|
|
||||||
|
- [x] Reduce up-front creation of all install app entries.
|
||||||
|
- [x] Keep category/app grouping as pure data before touching WPF controls.
|
||||||
|
- [x] Create WPF controls only on the UI thread.
|
||||||
|
- [x] Investigate replacing eager app-entry creation with item templates or incremental category rendering.
|
||||||
|
- [x] Verify search, selected-app counts, right-click popup, install, uninstall, and import/export selection state still work.
|
||||||
|
|
||||||
|
## P3 - Toggle And Registry Startup Cost
|
||||||
|
|
||||||
|
- [x] Stop doing expensive registry-backed toggle status checks while constructing invisible UI.
|
||||||
|
- [x] Defer toggle state reads until the Tweaks tab is first opened.
|
||||||
|
- [x] Review `Get-WinUtilToggleStatus` so a status read does not create missing registry paths unless that behavior is explicitly required.
|
||||||
|
- [x] Batch or cache repeated registry checks where possible.
|
||||||
|
- [x] Verify toggle import, undo, preset, and manual click behavior still match current expectations.
|
||||||
|
|
||||||
|
## P4 - Runspace Cleanup
|
||||||
|
|
||||||
|
- [x] Refactor `Invoke-WPFRunspace` to avoid shared `$script:powershell` and `$script:handle` state for concurrent callers.
|
||||||
|
- [x] Keep the shared runspace pool alive for the app lifetime instead of disposing it from individual queued calls.
|
||||||
|
- [x] Add explicit completion cleanup for each PowerShell instance after `EndInvoke`.
|
||||||
|
- [x] Add focused Pester coverage for multiple queued runspace calls, failures, and cleanup.
|
||||||
|
- [x] Do not parallelize winget/choco package installs by default; package manager locking and prompts make that unsafe.
|
||||||
|
|
||||||
|
## P5 - Defer Runspace Pool Startup
|
||||||
|
|
||||||
|
- [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
|
||||||
|
|
||||||
|
- [x] Measure `Invoke-WinUtilAssets -Render` calls during startup.
|
||||||
|
- [x] Cache rendered logo, checkmark, and warning images once per process.
|
||||||
|
- [x] Defer non-critical taskbar overlay assets until after first render if measurable.
|
||||||
|
- [x] Verify taskbar overlay success/error states still display correctly.
|
||||||
|
|
||||||
|
## P7 - Verification
|
||||||
|
|
||||||
|
- [x] Run `.\Compile.ps1`.
|
||||||
|
- [x] Run focused Pester tests for runspace, config, XAML wiring, install workflow, tweaks, and preferences/theme.
|
||||||
|
- [x] Run the full Pester gate before marking speed work complete.
|
||||||
|
- [ ] Manually launch with `.\Compile.ps1 -Run` and verify first paint, default tab, lazy tabs, search, install selection, tweak toggles, AppX tab, and Win11 ISO tab.
|
||||||
|
- [x] Check `git status --short` and do not stage or commit generated `winutil.ps1`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
function Start-WinUtilPerformanceTrace {
|
||||||
|
if (-not (Test-WinUtilPerformanceTrace) -or $null -eq $sync) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $sync.ContainsKey("PerformanceTrace")) {
|
||||||
|
$sync.PerformanceTrace = @{
|
||||||
|
Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
LastElapsedMs = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name "Startup trace started"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
function Stop-WinUtilPerformanceTrace {
|
||||||
|
param(
|
||||||
|
[string]$Name = "Startup trace complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-WinUtilPerformanceTrace) -or $null -eq $sync -or -not $sync.ContainsKey("PerformanceTrace")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilPerformanceCheckpoint -Name $Name
|
||||||
|
|
||||||
|
if ($null -ne $sync.PerformanceTrace.Stopwatch) {
|
||||||
|
$sync.PerformanceTrace.Stopwatch.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
function Test-WinUtilPerformanceTrace {
|
||||||
|
$enabledValue = ([string]$env:WINUTIL_PERF_LOG).ToLowerInvariant()
|
||||||
|
if ($enabledValue -in @("1", "true", "yes", "on")) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $sync -and $sync.ContainsKey("PerformanceTraceEnabled")) {
|
||||||
|
return [bool]$sync.PerformanceTraceEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
function Write-WinUtilPerformanceCheckpoint {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-WinUtilPerformanceTrace) -or $null -eq $sync) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $sync.ContainsKey("PerformanceTrace") -or $null -eq $sync.PerformanceTrace.Stopwatch) {
|
||||||
|
$sync.PerformanceTrace = @{
|
||||||
|
Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
LastElapsedMs = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elapsedMs = [math]::Round($sync.PerformanceTrace.Stopwatch.Elapsed.TotalMilliseconds, 2)
|
||||||
|
$deltaMs = [math]::Round($elapsedMs - [double]$sync.PerformanceTrace.LastElapsedMs, 2)
|
||||||
|
$sync.PerformanceTrace.LastElapsedMs = $elapsedMs
|
||||||
|
|
||||||
|
Write-WinUtilLog -Level "DEBUG" -Component "StartupPerf" -Message ("{0}: {1:N2} ms (+{2:N2} ms)" -f $Name, $elapsedMs, $deltaMs)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user