Creating a Function to Test and Compare PowerShell Commands

As you work with PowerShell, and especially when you begin developing scripts, you’ll realize there may be several ways to achieve the same result. How do you decide which is the better option? My first criteria is that the best choice best takes advantage of the PowerShell paradigm.

For example, you might have started with code like this, which is modeled after the way we would have used VBScript.

​
Ostensibly it works.
The results of our computer's properties. (Image Credit: Jeff Hicks)
The results of our computer's properties. (Image Credit: Jeff Hicks)
  But this isn't really good PowerShell. This a better approach to writing our PowerShell.
Our formatted result for our modified PowerShell code. (Image Credit: Jeff Hicks)
Our formatted result for our modified PowerShell code. (Image Credit: Jeff Hicks)
  The result is even formatted nicely. This is the first test. Unfortunately there really isn't a quantitative way to determine that the latter approach is better. This comes from experience. So let's assume that as you are working you have two equally valid and good PowerShell solutions to a task. Which is better? For me, the next test is legibility. That is, how easy is it to understand and follow what is happening in your code? Which makes more sense to you? Which is easier for someone else to understand? Again, this is a subjective measure. The final standard, at least for me, is performance. Normally I don't worry too much if one approach is a few milliseconds faster than another. Usually you can get a sense simply by running the different commands. But the best way is to actually measure the command expression and see how long each takes. Here are some simple examples. The task is to check the status of a list of services on a remote computer. Being new to PowerShell, you might come up with code like this:
​
It works just fine.
List of services on a computer. (Image Credit: Jeff Hicks)
List of services on a computer. (Image Credit: Jeff Hicks)
  Probably not the best PowerShell because Get-Service can accept an array of service names, which means this also works.
​
But let's find out which one truly is faster. We will do this with the Measure-Command cmdlet.
The Measure-Command cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
The Measure-Command cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
  Typically you use the command like this:
​
PowerShell runs the scriptblock,, in this case getting values from 1 to 10,000, and measures how long it takes.
Running the Measure-Command cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
Running the Measure-Command cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
  The result is a TimeSpan object. As you can see from the screenshot it took almost four seconds to complete. Note that you don't see any results from the command itself. So let's test our two service commands.
​
Here's the result:
Our test results. (Image Credit: Jeff Hicks)
Our test results. (Image Credit: Jeff Hicks)
  Now for the other command.
Our second test result. (Image Credit: Jeff Hicks)
Our second test result. (Image Credit: Jeff Hicks)
  Faster, but not as much as I thought. And in this case, there may be networking or server performance issues affecting the outcome. Additionally, depending on the commands you are testing, there may be a caching effect which could skew repeated tests. So while the results are meaningful you should take them in context. That said, I might get better information if I test the expression several times and take the average.
Results after we've tested our code several times. (Image Credit: Jeff Hicks)
Results after we've tested our code several times. (Image Credit: Jeff Hicks)
  Using a for loop, I measure how long it takes to run my scriptblock, saving the results to $Totals. To mitigate any caching or other issues, I'm sleeping for 10 seconds between each test. When finished, I can then calculate the average of the TotalMilliseconds property. Now to test the other:
Results after calculating milliseconds. (Image Credit: Jeff Hicks)
Results after calculating milliseconds. (Image Credit: Jeff Hicks)
  So the second approach is faster and better PowerShell. I like the idea of testing over a number of runs an reporting an average so much I created a PowerShell function to make it easier. I call it Test-Expression.
​
Function Test-Expression {
<#
.SYNOPSIS
Test a PowerShell expression.
.DESCRIPTION
This command will test a PowerShell expression or scriptblock for a specified number of times and calculate the average runtime, in milliseconds, over all the tests.
.PARAMETER Expression
The scriptblock you want to test. This parameter has an alias of sb.
.PARAMETER ArgumentList
An array of parameters to pass to the scriptblock. Arguments are positional.
.PARAMETER Count
The number of times to test the scriptblock.
.PARAMETER Interval
How much time to sleep in seconds between each test. Maximum is 60. You may want to use a sleep interval to mitigate possible caching effects.
.PARAMETER AsJob
Run the tests as a background job.
.EXAMPLE
PS C:\> Test-Expression {param($cred) get-wmiobject win32_logicaldisk -computer chi-dc01 -credential $cred } -argumentList $cred
Tests        : 1
TestInterval : 0.5
AverageMS    : 146.9054
MinimumMS    : 146.9054
MaximumMS    : 146.9054
Test a command once passing an argument to the scriptblock.
.EXAMPLE
PS C:\> $sb = {1..1000 | foreach {$_*2}}
PS C:\> test-expression $sb -count 10 -interval 2
Tests        : 10
TestInterval : 2
AverageMS    : 24.78196
MinimumMS    : 21.0534
MaximumMS    : 36.2801
PS C:\> $sb2 = { foreach ($i in (1..1000)) {$_*2}}
PS C:\> test-expression $sb2 -Count 10 -interval 2
Tests        : 10
TestInterval : 2
AverageMS    : 2.62559
MinimumMS    : 0.4883
MaximumMS    : 15.1753
These examples are testing two different approaches that yield the same results over a span of 10 test runs, pausing for 2 seconds between each test. The values for Average, Minimum and Maximum are in milliseconds.
.NOTES
NAME        :  Test-Expression
VERSION     :  1.0
LAST UPDATED:  2/13/2015
AUTHOR      :  Jeff Hicks
Learn more about PowerShell:
Essential PowerShell Learning Resources
**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .LINK Measure-Command Measure-Object .INPUTS None .OUTPUTS Custom measurement object #> [cmdletbinding()] Param( [Parameter(Position=0,Mandatory,HelpMessage="Enter a scriptblock to test")] [Alias("sb")] [scriptblock]$Expression, [object[]]$ArgumentList, [ValidateScript({$_ -ge 1})] [int]$Count=1, [ValidateRange(0,60)] [double]$Interval = .5, [switch]$AsJob ) Write-Verbose "Measuring expression:" Write-Verbose ($Expression | Out-String) write-Verbose "$Count time(s) with a sleep interval of $($interval*1000) milliseconds" <# define an internal scriptblock that can be used directly or used to create a background job #> $myScriptBlock = { 1..$using:count | foreach -begin { <# PowerShell doesn't seem to like passing a scriptblock as an argument when using Invoke-Command. It appears to pass it as a string so I'm recreating it as a scriptblock here. #> $script:testblock = [scriptblock]::Create($using:Expression) } -process { #invoke the scriptblock with any arguments and measure Measure-Command -Expression { Invoke-Command -ScriptBlock $script:testblock -ArgumentList @($using:ArgumentList) } #pause to mitigate any caching effects Start-Sleep -Milliseconds ($using:Interval*1000) } | Measure-Object -Property TotalMilliseconds -Average -Maximum -Minimum | Select-Object -Property @{Name="Tests";Expression={$_.Count}}, @{Name="TestInterval";Expression={$using:Interval}}, @{Name="AverageMS";Expression={$_.Average}}, @{Name="MinimumMS";Expression={$_.Minimum}}, @{Name="MaximumMS";Expression={$_.Maximum}} } #myScriptBlock #parameter hashtable to splat against Invoke-Command $paramHash = @{ ScriptBlock = $myScriptBlock ComputerName = $env:computername HideComputerName = $True } If ($AsJob) { Write-Verbose "Running as a background job" $paramHash.Add("AsJob",$True) } #exclude RunspaceID where possible Invoke-Command @paramHash | Select-Object -property * -ExcludeProperty RunspaceID } #end function

The premise is essentially what I have just demonstrated, although the implementation is a bit advanced. This should make it easier to test and compare PowerShell commands. You can specify the number of times to test and how many seconds to sleep between tests. The following tests produce the same result but one is significantly faster than the other.

Test-Expression function results. (Image Credit: Jeff Hicks)
Test-Expression function results. (Image Credit: Jeff Hicks)
  Compared to this:
021315 2011 TheMeasureo11
And finally this example using new syntax in PowerShell 4.0.
021315 2011 TheMeasureo12
You might have other requirements that make one of these more attractive than the others but at least in terms of measurable performance you can make an informed decision.