Slow -AsJob in Get-ReferencedCommand

Sep 2, 2013 at 5:47 PM
Edited Sep 2, 2013 at 6:08 PM
I seem to remember running into this problem before, but now I've tracked it down enough to discuss. Repro is simple: the following command seems to take an eternity:
new-button "Close me" -Show -AsJob
Procmon shows me that powershell is opening tons of files in the $env:psmodulepath. I managed to track this down to Start-WPFJob calling Get-ReferencedCommand. Actual execution time for Get-ReferencedCommand is 215 seconds.

I'd like to take a stab at rewriting Get-ReferencedCommand this to speed it up, but find it hard to debug in its current mixed language form.
Sep 3, 2013 at 12:19 AM
Edited Sep 3, 2013 at 1:09 AM
This may be particularly painful on my machine because I've got a network share in my $env:path, but the current implementation seems so slow as to be flawed.

I've written a PowerShell 3 only replacement Get-ReferencedCommand which is much faster: 0.79 seconds vs. 215 seconds (on my machine.)

Does ShowUI already have a PowerShell 3 dependency? I'm using the ScriptBlock AST feature below.
function Get-ReferencedCommand { 
    <#
    .Synopsis
        Gets the commands referred to from within a ScriptBlock
    .Description
        Uses the ScriptBlock's AST to to get the commands referred to (recursively)     
    .Example
        { Show-Window } | Get-ReferencedCommand
    #>
    param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$true)]
        [ScriptBlock] $ScriptBlock, # The script block to search for command references

        [string[]]$Exclude = @('%'),

        [switch]$Recurse = $true
    ) 

    begin {
        Set-StrictMode -Version 3
        Write-Verbose "Get-ReferencedCommand begin"
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $queue = new-object `
            "System.Collections.Generic.Queue[[ScriptBlock]]"
        $cachedCommandName = new-object `
            "System.Collections.Generic.HashSet[[string]]"
        $cachedCommandInfo = new-object `
            "System.Collections.Generic.HashSet[[System.Management.Automation.CommandInfo]]"
    }

    process {   
        $queue.Enqueue($ScriptBlock)
    }

    end {
        function ProcessCommand {
            param (
                [Parameter(Mandatory=$true)]
                [System.Management.Automation.CommandInfo]
                $cmd
                )
            if ($cmd -and !$cachedCommandInfo.Contains($cmd) -and $cmd.Name -notin $Exclude) {
                [void]$cachedCommandInfo.Add(($cmd))
                Write-Output $cmd
                switch ($cmd.CommandType) {
                    Alias    { ProcessCommand $cmd.ResolvedCommand }
                    Function { if ($Recurse) { $queue.Enqueue($cmd.ScriptBlock) }}
                    Filter   { if ($Recurse) { $queue.Enqueue($cmd.ScriptBlock) }}
                    ExternalScript { 
                        if ($Recurse) { 
                            try {
                                $ScriptBlock = $cmd.ScriptBlock
                                if (!$ScriptBlock) { $cmd.ValidateScriptInfo($null) }
                                  $queue.Enqueue($ScriptBlock) 
                            } catch [Management.Automation.PSSecurityException] {
                                Write-Warning $_
                            }
                        }
                    }
                }
            }
        }

        while (!$pscmdlet.Stopping -and $queue.Count) {
            $queue.Dequeue().Ast.FindAll( { 
                !$pscmdlet.Stopping -and 
                    $args[0] -is [Management.Automation.Language.CommandAst] 
             }, $true ) | % { 
                $node = $_
                try {
                    if ($node.InvocationOperator -eq "Unknown") {
                        $name = $node.CommandElements[0].value
                        if (!$cachedCommandName.Contains($name)) {
                            [void]$cachedCommandName.Add($name)
                            $cmd = Get-Command $name -ErrorAction SilentlyContinue -ErrorVariable GetCommandError
                            if ($cmd) {
                                ProcessCommand $cmd 
                            } else { 
                                $location = if ($_.Extent.File) { $_.Extent.File } else { "<ScriptBlock>" }
                                $location += ":$($_.Extent.StartLineNumber),$($_.Extent.StartColumnNumber)"
                                Write-Warning "$GetCommandError`r`n at $location" 
                            }
                        }
                    }
                } catch { 
                    Write-Warning "Unexpected warning processing command $node : $_"
                }
            }
        }
        $stopwatch.Stop();
        Write-Verbose "Get-ReferencedCommand end ($($stopwatch.Elapsed.TotalSeconds) s)"
    }
}
Sep 3, 2013 at 3:52 AM
Pull request submitted.
Coordinator
Sep 3, 2013 at 4:44 AM
BurtHarris wrote:
Pull request submitted.
Awesome, I'll take a look later this week (working on a release for this this week, hopefully) We don't have a PS3 dependency (yet), everything should work in 2.0 -- but that doesn't mean that I can't do version detection when defining the function if there's something that would make that much of a difference!
Coordinator
Sep 5, 2013 at 7:43 PM
Edited Sep 5, 2013 at 7:43 PM
I pulled and merged this -- should be in 1.5 -- everything tests ok so far, and faster, as you said :-)