The Series
Welcome back for more fun with PowerShell and XAML ProgressBars! Today we are going to tackle performance tuning all on it’s own.
The Problem
If you attempted to run the demo at the end of PowerShell ProgressBar – Part 1, and you were sneaky enough to remove my Start-Sleep cmdlet you may have noticed that the performance is AWFUL!
Here are the stats just running the following progress bar demo code (Start-Sleep removed):
1..100 | foreach {Write-ProgressBar -ProgressBar $ProgressBar -Activity "Counting $_ out of 100" -PercentComplete $_ }
50 seconds just to count to 100 is a lot of overhead to a script just to add a progress bar. In most cases end users want speed over shininess so we are going to need to fix this.
##The Solution Doing a quick Google search turned up the fact that other people had experienced the same issue when using dispatcher.invoke1 it turns out this is because dispatcher.invoke is thread blocking (i.e. your script has to wait for the invoke function, which has quite a bit of overhead, to complete before it will continue).
Turns out the there are a few other non-threadblocking methods for updating your GUI. I chose to use something called the dispatchertimer because of this dandy article2 and a sweet simple demo from Richard Siddaway3.
What the dispatchertimer allows us to do is tell our GUI to run some code on a set interval. So, I want my GUI to update every ten milliseconds to reflect the properties on my $Synchash and this should dramatically increase our performance.
So, to walk it through. We added a property for the activity and a property to for the percentcomplete to our $Synchash
$syncHash.Activity = ''
$syncHash.PercentComplete = 0
We then created a scriptblock to that we wanted to run at the set interval which will update the GUI.
$updateBlock = {
$SyncHash.Window.Title = $SyncHash.Activity
$SyncHash.ProgressBar.Value = $SyncHash.PercentComplete
}
Then we will create the dispatchtimer which will call the code and set the interval to be 10 milliseconds.
$syncHash.Window.Add_SourceInitialized( {
## Before the window's even displayed ...
## We'll create a timer
$timer = new-object System.Windows.Threading.DispatcherTimer
## Which will fire 4 times every second
$timer.Interval = [TimeSpan]"0:0:0.01"
## And will invoke the $updateBlock
$timer.Add_Tick( $updateBlock )
## Now start the timer running
$timer.Start()
if( $timer.IsEnabled ) {
Write-Host "Clock is running. Don't forget: RIGHT-CLICK to close it."
} else {
$clock.Close()
Write-Error "Timer didn't start"
}
} )
This makes my Write-ProgressBar cmdlet as simple as changing a property on the variable.
function Write-ProgressBar
{
Param (
[Parameter(Mandatory=$true)]
$ProgressBar,
[Parameter(Mandatory=$true)]
[String]$Activity,
[int]$PercentComplete
)
$ProgressBar.Activity = $Activity
if($PercentComplete)
{
$ProgressBar.PercentComplete = $PercentComplete
}
}
The end results in performance are… Drumroll please!!!
Full Code
Function New-ProgressBar {
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$syncHash.Runspace = $newRunspace
$syncHash.Activity = ''
$syncHash.PercentComplete = 0
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$data = $newRunspace.Open() | Out-Null
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$PowerShellCommand = [PowerShell]::Create().AddScript({
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Window" Title="Progress..." WindowStartupLocation = "CenterScreen"
Width = "300" Height = "100" ShowInTaskbar = "True">
<StackPanel Margin="20">
<ProgressBar Name="ProgressBar" />
<TextBlock Text="{Binding ElementName=ProgressBar, Path=Value, StringFormat={}{0:0}%}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</StackPanel>
</Window>
"@
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================
$xaml.SelectNodes("//*[@Name]") | %{ $SyncHash."$($_.Name)" = $SyncHash.Window.FindName($_.Name)}
$updateBlock = {
$SyncHash.Window.Title = $SyncHash.Activity
$SyncHash.ProgressBar.Value = $SyncHash.PercentComplete
}
############### New Blog ##############
$syncHash.Window.Add_SourceInitialized( {
## Before the window's even displayed ...
## We'll create a timer
$timer = new-object System.Windows.Threading.DispatcherTimer
## Which will fire 4 times every second
$timer.Interval = [TimeSpan]"0:0:0.01"
## And will invoke the $updateBlock
$timer.Add_Tick( $updateBlock )
## Now start the timer running
$timer.Start()
if( $timer.IsEnabled ) {
Write-Host "Clock is running. Don't forget: RIGHT-CLICK to close it."
} else {
$clock.Close()
Write-Error "Timer didn't start"
}
} )
$syncHash.Window.ShowDialog() | Out-Null
$syncHash.Error = $Error
})
$PowerShellCommand.Runspace = $newRunspace
$data = $PowerShellCommand.BeginInvoke()
Register-ObjectEvent -InputObject $SyncHash.Runspace `
-EventName 'AvailabilityChanged' `
-Action {
if($Sender.RunspaceAvailability -eq "Available")
{
$Sender.Closeasync()
$Sender.Dispose()
}
} | Out-Null
return $syncHash
}
function Write-ProgressBar
{
Param (
[Parameter(Mandatory=$true)]
$ProgressBar,
[Parameter(Mandatory=$true)]
[String]$Activity,
[int]$PercentComplete
)
$ProgressBar.Activity = $Activity
if($PercentComplete)
{
$ProgressBar.PercentComplete = $PercentComplete
}
}
function Close-ProgressBar
{
Param (
[Parameter(Mandatory=$true)]
[System.Object[]]$ProgressBar
)
$ProgressBar.Window.Dispatcher.Invoke([action]{
$ProgressBar.Window.close()
}, "Normal")
}
Demo
#Put a Start-Sleep back in if you actually want to see the progress bar up.
$ProgressBar = New-ProgressBar
Measure-Command -Expression {
1..100 | foreach {Write-ProgressBar -ProgressBar $ProgressBar -Activity "Counting $_ out of 100" -PercentComplete $_}
}
Close-ProgressBar $ProgressBar