Adding Nodes to TreeView is very slow unless I'm using custom c# code

Hi,

The problem: Adding Nodes to TreeView is very slow if I’m using PowerShell code that invokes the control methods. The same code used via custom c# types is very fast.

Why launching the same tasks from PS is so slow? Just run the below code, click one of the buttons to see a huge difference. Does anyone have an idea how it could be improved without relying on custom c# code?

Code: Untitled (a4py9egg) - PasteCode.io

using namespace System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()

$form1 = New-Object 'System.Windows.Forms.Form'
$buttonRunPS = New-Object 'System.Windows.Forms.Button'
$buttonRunC = New-Object 'System.Windows.Forms.Button'
$richtextbox2 = New-Object 'System.Windows.Forms.RichTextBox'
$richtextbox1 = New-Object 'System.Windows.Forms.RichTextBox'
$treeview1 = New-Object 'System.Windows.Forms.TreeView'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'

function Start-RunspaceTask {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, Position = 0)][ScriptBlock]$ScriptBlock,
        [Parameter(Mandatory, Position = 1)][PSObject[]]$ProxyVars
    )
    
    $Runspace = [RunspaceFactory]::CreateRunspace($InitialSessionState)
    $Runspace.ApartmentState = 'STA'
    $Runspace.ThreadOptions = 'ReuseThread'
    $Runspace.Open()
    ForEach ($Var in $ProxyVars) { $Runspace.SessionStateProxy.SetVariable($Var.VariableName, $Var.VariableValue) }
    $Thread = [PowerShell]::Create()
    $Thread.AddScript($ScriptBlock, $True) | Out-Null
    $Thread.Runspace = $Runspace
    [Void]$jobList.Add([PSObject]@{ PowerShell = $Thread; Runspace = $Thread.BeginInvoke() })
}

#System.Collections.Concurrent is thread-safe
$global:SyncHash = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new()
$global:jobList = [System.Collections.Concurrent.ConcurrentBag[object]]::new()

$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$SyncHash.form1 = $form1
$SyncHash.treeview1 = $treeview1
$SyncHash.richtextbox1 = $richtextbox1
$SyncHash.richtextbox2 = $richtextbox2

$buttonRunC_Click = {
#region Custom Types
    Add-Type -ReferencedAssemblies System.Windows.Forms, System.ComponentModel, System.ComponentModel.Primitives, System.Threading.Tasks -TypeDefinition @'
using System.Threading.Tasks;
using System.Windows.Forms;

public class TreeViewHelper
{
    public static void AddNode(TreeView TreeView, TreeNode TreeNode)
    {
        TreeView.Invoke((MethodInvoker)(() => TreeView.Nodes.Add(TreeNode)));
    }

    public static async void AddNodeAsync(TreeView TreeView, string Name)
    {
        await Task.Run(() =>
        {
            TreeView.Invoke((MethodInvoker)(() => TreeView.Nodes.Add(Name)));
        });
    }
}
'@
    
    Add-Type -ReferencedAssemblies System.Windows.Forms, System.ComponentModel, System.ComponentModel.Primitives, System.Threading.Tasks -TypeDefinition @'
using System.Threading.Tasks;
using System.Windows.Forms;

public class RichTextBoxHelper
{
    public static void AddText(RichTextBox RichTextBoxControl, string Text)
    {
        RichTextBoxControl.Invoke((MethodInvoker)(() => RichTextBoxControl.AppendText(Text)));
    }

    public static async void AddTextAsync(RichTextBox RichTextBoxControl, string Text)
    {
        await Task.Run(() =>
        {
            RichTextBoxControl.Invoke((MethodInvoker)(() => RichTextBoxControl.AppendText(Text)));
        });
    }
}
'@
#endregion 

    $ScriptBlock = {
        $treeview1.Nodes.Clear() | Out-Null
        
        Get-ChildItem Function: | Where-Object { $_.Name -NotLike "*:*" } | Select-Object -ExpandProperty Name | ForEach-Object {
            $Definition = Get-Content "function:$_" -ErrorAction Stop
            $SessionStateFunction = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList "$_", $Definition
            $InitialSessionState.Commands.Add($SessionStateFunction)
        }
        
        $TopLevelDir = 'C:\Windows\System32'
        
        foreach ($FilePath in [System.Io.Directory]::EnumerateFiles($TopLevelDir, "*.dll", [System.IO.SearchOption]::AllDirectories)) {
            $counter = 0
            
            $streamReader = [System.IO.StreamReader]::new($FilePath, [System.Text.Encoding]::Default)
            
            while ($null -ne ($line = $streamReader.ReadLine()) -and $counter -le 100) {
                #simulate read file work
                $counter++
            }
            
            $streamReader.Close()
            $streamReader.Dispose()
            
            [RichTextBoxHelper]::AddText($SyncHash.richtextbox2, $FilePath)
            [TreeViewHelper]::AddNode($SyncHash.treeview1, $FilePath) # works from Runspace
        }
    }
    
    Start-RunspaceTask $ScriptBlock -ProxyVars @([PSObject]@{ VariableName = 'SyncHash'; VariableValue = $SyncHash })
    #$ScriptBlock.invoke() #replace Start-RunspaceTask with this line if you want to debug code
}

$buttonRunPS_Click = {

    $ScriptBlock = {
        $treeview1.Nodes.Clear() | Out-Null
        
        Get-ChildItem Function: | Where-Object { $_.Name -NotLike "*:*" } | Select-Object -ExpandProperty Name | ForEach-Object {
            $Definition = Get-Content "function:$_" -ErrorAction Stop
            $SessionStateFunction = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList "$_", $Definition
            $InitialSessionState.Commands.Add($SessionStateFunction)
        }
        
        $TopLevelDir = 'C:\Windows\System32'
        
        foreach ($FilePath in [System.Io.Directory]::EnumerateFiles($TopLevelDir, "*.dll", [System.IO.SearchOption]::AllDirectories)) {
            $counter = 0
            
            $streamReader = [System.IO.StreamReader]::new($FilePath, [System.Text.Encoding]::Default)
            
            while ($null -ne ($line = $streamReader.ReadLine()) -and $counter -le 100) {
                #simulate read file work
                $counter++
            }
            
            $streamReader.Close()
            $streamReader.Dispose()
            
            $SyncHash.richtextbox2.Invoke([System.Windows.Forms.MethodInvoker]{ $SyncHash.richtextbox2.AppendText($FilePath + "`n") }) # works from Runspace but very slow
            $SyncHash.treeview1.Invoke([System.Windows.Forms.MethodInvoker]{ $SyncHash.treeview1.Nodes.Add($FilePath) }) # works from Runspace but very slow

        }
    }
    
    Start-RunspaceTask $ScriptBlock -ProxyVars @([PSObject]@{ VariableName = 'SyncHash'; VariableValue = $SyncHash })
    #$ScriptBlock.invoke() #replace Start-RunspaceTask with this line if you nwat to debug
}

#region Form Code
$form1.SuspendLayout()
$form1.Controls.Add($buttonRunPS)
$form1.Controls.Add($buttonRunC)
$form1.Controls.Add($richtextbox2)
$form1.Controls.Add($richtextbox1)
$form1.Controls.Add($treeview1)
$form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$form1.AutoScaleMode = 'Font'
$form1.ClientSize = New-Object System.Drawing.Size(548, 647)
$form1.Name = 'form1'
$form1.Text = 'Form'
$buttonRunPS.Anchor = 'Bottom, Right'
$buttonRunPS.Location = New-Object System.Drawing.Point(432, 612)
$buttonRunPS.Name = 'buttonRunPS'
$buttonRunPS.Size = New-Object System.Drawing.Size(104, 23)
$buttonRunPS.TabIndex = 4
$buttonRunPS.Text = 'Run PS'
$buttonRunPS.UseVisualStyleBackColor = $True
$buttonRunPS.add_Click($buttonRunPS_Click)
$buttonRunC.Anchor = 'Bottom, Left'
$buttonRunC.Location = New-Object System.Drawing.Point(304, 612)
$buttonRunC.Name = 'buttonRunC'
$buttonRunC.Size = New-Object System.Drawing.Size(122, 23)
$buttonRunC.TabIndex = 3
$buttonRunC.Text = 'Run C#'
$buttonRunC.UseVisualStyleBackColor = $True
$buttonRunC.add_Click($buttonRunC_Click)
$richtextbox2.Anchor = 'Top, Bottom, Left, Right'
$richtextbox2.Location = New-Object System.Drawing.Point(304, 295)
$richtextbox2.Name = 'richtextbox2'
$richtextbox2.Size = New-Object System.Drawing.Size(232, 311)
$richtextbox2.TabIndex = 2
$richtextbox2.Text = ''
$richtextbox1.Anchor = 'Top, Left, Right'
$richtextbox1.Location = New-Object System.Drawing.Point(303, 12)
$richtextbox1.Name = 'richtextbox1'
$richtextbox1.Size = New-Object System.Drawing.Size(233, 276)
$richtextbox1.TabIndex = 1
$richtextbox1.Text = ''
$treeview1.Anchor = 'Top, Bottom, Left'
$treeview1.Location = New-Object System.Drawing.Point(12, 12)
$treeview1.Name = 'treeview1'
$treeview1.Size = New-Object System.Drawing.Size(285, 623)
$treeview1.TabIndex = 0
$form1.ResumeLayout()
#endregion

$form1.ShowDialog()

Adding my response from discord:

yep! I’m surprised it works at all tbh. Reason it’s slow is because scriptblocks have runspace affinity. They’re tied to the runspace they’re created in. When you wrap them in a delegate and invoke them in another thread, they try to marshal back to the origin runspace

most of the time they fail to do so after a 500ms (?) timeout

and at that point they pretend that they’re on the right thread and access their runspace state unsafely

that also assumes that the thread the delegate gets invoked on happens to have a default runspace that is different

if it has no default runspace, it just throws

my advice is to keep using the C# work around

Or maybe try

$SyncHash.richtextbox2.Invoke([System.Windows.Forms.MethodInvoker]{ $SyncHash.richtextbox2.AppendText($FilePath + "`n") }.Ast.GetScriptBlock())
1 Like