Measuring and Optimising PowerShell Script Performance

I’ve been meaning to experiment a bit with measuring how well PowerShell scripts run and what difference optimisations can make on the running time.  So it’s time to jolly-well do it.

Enter;  Measure-Command (I’m mentally playing the music from Enter the Dragon while writing that)

Measure-Command is pretty straight forward;  pass a PowerShell command or script to it (in the form of a script block) and it returns how long it took to complete.  In order to make some kind of comparison I needed to measure both the script I wrote to sync a folder (see here)  and a new version of the same script.  The later script doesn’t use many built-in PowerShell tricks like the first one uses and is a more direct, ‘brute-force’ attempt perform the same task.  The new script is attached at the end.

A straight comparison of running time seems like the best way forward but I might get  spurious results if the server happens to be a bit busy when I run the command.  So I need to run the command multiple times and take an average of the results.  Luckily, PowerShell makes this pretty easy.

1..20 | % {(Measure-Command {Sync-Folder D:\Source D:\Target}).TotalSeconds} |
 Measure-Object -Average

Here I pipe 20 numbers from 1 to 20 to a ForEach-Object(%)  cmdlet.  The numbers aren’t used, but it repeats the script block 20 times.  I run my Measure-Command on my function and take the TotalSeconds property of the result.  These are piped to Measure-Object which is configured to average all the results it gets.  This means the results of 20 Measure-Commands are averaged and outputted.

I ran both the scripts on the same directory and I had previously copied quite a bit of data in the source folder to make sure the scripts did a little work.

What I found was pretty interesting;  my original script ran in 0.73 seconds on average while the new, brute-force script took 1.34 seconds.   That’s a pretty big difference!

So what accounts for the difference?  I think the main thing is that the original script takes advantage of built-in PowerShell functionality.  Filtering (passing results to Where-Object (the alias ?)  for example is very efficient and reduces the number of times the script has to loop (it only loops over the objects we’re interested in rather than all the objects in the folder).

function Sync-Folder
{
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory=$True)]
        [string]$SourceFolder,
        [parameter(Mandatory=$True)]
        [string]$TargetFolder

    )
    function Sync-OneFolder
    {
        param
        (
            [parameter(Mandatory=$True,ValueFromPipeline=$True)]
            [string]$SourceFolder,
            [parameter(Mandatory=$True)]
            [string]$TargetFolder

        )
        Write-Verbose "Source Folder : $SourceFolder"
        Write-Verbose "Target Folder : $TargetFolder"
        if (!(Test-Path -Path $TargetFolder -PathType Container))
        {
            Write-Verbose "Creating Folder : $($TargetFolder)"
            New-Item $TargetFolder -ItemType "Directory" > $null
        }

        $TargetList=gci $TargetFolder
        foreach ($TargetItem in $TargetList)
        {
            if (!(Test-Path ($SourceFolder+"\"+$TargetItem.Name)))
            {
                Write-Verbose "Removing file missing from Source : $($TargetItem.FullName)"
                Remove-Item ($TargetItem.FullName)
            }
        }
        $SourceList=gci $SourceFolder
        foreach ($SourceItem in $SourceList)
        {
            if (Test-Path $SourceItem.FullName -PathType Container)
            {
                 Sync-OneFolder $SourceItem.FullName ($TargetFolder+"\"+$SourceItem.Name)
            }else
            {
                if (Test-Path ($TargetFolder+"\"+$SourceItem.Name))
                {
                    if ($SourceItem.LastWriteTime -gt (gci ($TargetFolder+"\"+$SourceItem.Name)).LastWriteTime)
                    {
                        Write-Verbose "Copying updated file : $($TargetFolder+"\"+$SourceItem.Name)"
                        Copy-Item ($SourceItem.FullName) ($TargetFolder+"\"+$SourceItem.Name) -Force
                    }
                }else
                {
                    Write-Verbose "Copying missing file : $($TargetFolder+"\"+$SourceItem.Name)"
                    Copy-Item ($SourceItem.FullName) ($TargetFolder+"\"+$SourceItem.Name)
                }

            }

        }

    }
    Sync-OneFolder $SourceFolder $TargetFolder
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: