4

I'm an active reader of StackOverflow as it usually helps me resolve all my issues.
But for my first question ever I'll ask your help about runspaces in Powershell, specifically for memory management.

I created some PS apps with GUI and now I use runspaces to avoid hang of the GUI when big operations are running. My problem is that I can't manage to dispose my runspaces when they are finished.

Regarding the code below, it's a simple script to try to understand how I can dispose of my runspaces. I tried to incorporate a unique runspace to monitor and dispose the app runspaces, I inspired myself from a proxb's script (huge fan btw) but it doesn't seems to work.

Everytime I execute the runspace I gain 10Mb of memory, huge leak!

leak

Can you help me to resolve this problem please ?

[xml]$xaml = @'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Runspace" FontFamily="Calibri" Height="400" Width="400" FontSize="14">
    <Grid Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="10"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBox x:Name="TB_Results" Grid.Row="0" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
        <Button x:Name="BT_Run" Grid.Row="2" Content="Run" Height="30"/>
    </Grid>
</Window>
'@

# Inits
Add-Type -AssemblyName PresentationFramework            
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
$gui = [hashtable]::Synchronized(@{})
$reader = (New-Object Xml.XmlNodeReader $xaml)
$gui.Window = [Windows.Markup.XamlReader]::Load($reader)
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object { $gui.Add($_.Name,$gui.Window.FindName($_.Name)) }
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))
$Script:JobCleanup = [hashtable]::Synchronized(@{ Host = $host })

# Test function
function RunFunc {
        $newRunspace = [runspacefactory]::CreateRunspace()
        $newRunspace.ApartmentState = "STA"
        $newRunspace.ThreadOptions = "ReuseThread"
        $newRunspace.Open()
        $newRunspace.SessionStateProxy.SetVariable("gui",$gui)
        $Code = {
            $memupdate = "Memory: $($(Get-Process -Id $PID).PrivateMemorySize64 / 1mb) mb"
            $gui.Window.Dispatcher.invoke("Normal",[action]{
                $gui.TB_Results.Text = "$memupdate`r`n" + $gui.TB_Results.Text
            })
        }
        $Powershell = [powershell]::Create().AddScript($Code)
        $Powershell.Runspace = $newRunspace
        [void]$Script:Jobs.Add((
            [PSCustomObject]@{
                PowerShell = $PowerShell
                Runspace = $PowerShell.BeginInvoke()
            }
        ))
}

# Background Runspace to clean up jobs
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open() 
$newRunspace.SessionStateProxy.SetVariable("jobs",$Script:Jobs) 
$Code = {
    $JobCleanup = $true
    do {    
        foreach($runspace in $jobs) {            
            if ($runspace.Runspace.isCompleted) {
                $runspace.powershell.EndInvoke($runspace.Runspace) | Out-Null
                $runspace.powershell.dispose()
                $runspace.Runspace = $null
                $runspace.powershell = $null               
            } 
        }
        $temphash = $jobs.clone()
        $temphash | Where-Object { $_.runspace -eq $Null } | ForEach-Object { $jobs.remove($_) }        
        Start-Sleep -Seconds 1     
    } while ($JobCleanup)
}
$Powershell = [powershell]::Create().AddScript($Code)
$Powershell.Runspace = $newRunspace
$PowerShell.BeginInvoke()

# gui events
$gui.BT_Run.add_Click({ RunFunc })
$gui.Window.ShowDialog() | Out-Null

1 Answers1

4

When you call $runspace.PowerShell.Dispose(), the PowerShell instance is disposed, but the runspace that it's tied to is not automatically disposed, you'll need to do that first yourself in the cleanup task:

$runspace.powershell.EndInvoke($runspace.Runspace) | Out-Null
$runspace.powershell.Runspace.Dispose() # remember to clean up the runspace!
$runspace.powershell.dispose()
Mathias R. Jessen
  • 106,010
  • 8
  • 112
  • 163
  • 1
    @Carlton2001 great to hear! For your own sake, I'd suggest renaming the `Runspace` property in your custom job object to `Result` or `Handle`, and then change `foreach($runspace in $jobs)` to `foreach($job in $jobs)` – Mathias R. Jessen Aug 11 '18 at 13:27