Register for Semperis' Hybrid Identity Protection (HIP) Conference - June 30 - July 1 Register for Semperis' Hybrid Identity Protection (HIP) Conference - June 30 - July 1
PowerShell

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.

$c = Get-WMIObject win32_computersystem
$properties = "Manufacturer","Model","Name","TotalPhysicalMemory","HyperVisorPresent"
foreach ($property in $properties) {
"$Property = $($c.$property)"
}

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.

$c = Get-WMIObject win32_computersystem
$properties = "Manufacturer","Model","Name","TotalPhysicalMemory","HyperVisorPresent"
$c | Select-Object -Property $properties

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:

$services = "bits","wuauserv","winrm","w32time","remoteregistry"
foreach ($service in $services) {
 get-service $service -ComputerName chi-dc04
}

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.

get-service -name $services -ComputerName chi-dc04

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:

Measure-Command {1..10000}

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.

Measure-Command {
$services = "bits","wuauserv","winrm","w32time","remoteregistry"
foreach ($service in $services) {
 get-service $service -ComputerName chi-dc04
}
}

Here’s the result:

Our test results. (Image Credit: Jeff Hicks)
Our test results. (Image Credit: Jeff Hicks)

 

Now for the other command.

Measure-Command {
$services = "bits","wuauserv","winrm","w32time","remoteregistry"
get-service -name $services -ComputerName chi-dc04
}
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.

$Totals = for ($i=1; $i -le 5; $i++) {
Measure-Command {
$services = "bits","wuauserv","winrm","w32time","remoteregistry"
foreach ($service in $services) {
get-service $service -ComputerName chi-dc04
}
}
Start-Sleep -Seconds 10
}
$Totals | Measure-Object -Property TotalMilliseconds -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:

$Totals2 = for ($i=1; $i -le 5; $i++) {
Measure-Command {
$services = "bits","wuauserv","winrm","w32time","remoteregistry"
get-service -name $services -ComputerName chi-dc04
}
Start-Sleep -Seconds 10
}
$Totals2 | Measure-Object -Property TotalMilliseconds -Average
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.

#requires -version 4.0

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:
http://jdhitsolutions.com/blog/essential-powershell-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 {1..1000 | foreach {$_*2}} –count 10 –interval 1
Test-Expression function results. (Image Credit: Jeff Hicks)
Test-Expression function results. (Image Credit: Jeff Hicks)

 

Compared to this:

Test-Expression {foreach ($i in (1..1000)) { $i*2}} -count 10 -Interval 1

And finally this example using new syntax in PowerShell 4.0.

Test-Expression { (1..1000).foreach({$_*2})} -count 10 -Interval 1

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.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

Register for the Hybrid Identity Protection (HIP) Europe Conference!

Hybrid Identity Protection (HIP) Europe 2021 - Virtual Conference

Mobile workforces, cloud applications, and digitalization are changing every aspect of the modern enterprise. And with radical transformation come new business risks. Hybrid Identity Protection (HIP) is the premier educational forum for identity-centric practitioners. At the inaugural HIP Europe, join your local IAM experts and Microsoft MVPs to learn all the latest from the Hybrid Identity world.