Measure-Benchmark.ps1

function Measure-Benchmark
{
    <#
    .Synopsis
        Measures a performance benchmark
    .Description
        Measures the timing of a performance benchmark, or compares the timing of multiple techniques.
    .Example
        Measure-Benchmark -Command Get-Process -RepeatCount 10
    .Example
        Measure-Benchmark -ScriptBlock { Get-Process } -RepeatCount 10
    .Example
        Measure-Benchmark -Technique @{
            "Get-Process" = { Get-Process }
            '[Process]::GetProcesses()' = {
                [Diagnostics.Process]::GetProcesses()
            }
        } -GroupName "Fastest Way to Enumerate Processes"
    .Link
        Get-Benchmark
    .Link
        Measure-Benchmark
    #>

    [CmdletBinding(DefaultParameterSetName='Technique')]
    [OutputType([PSObject])]
    param(
    # A command to run. This can be the name of a command, or the text content of a script block.
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='Command',ValueFromPipelineByPropertyName=$true)]
    [Alias('Fullname')]
    [string]
    $Command,

    # A script block to execute.
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='ScriptBlock',ValueFromPipeline=$true)]
    [ScriptBlock]
    $ScriptBlock,

    # A hashtable of parameters. These can be passed to either a command or a script block.
    [Parameter(ParameterSetName='Command',ValueFromPipelineByPropertyName=$true)]
    [Parameter(ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName=$true)]
    [Alias('Parameters')]
    [Collections.IDictionary]
    $Parameter = @{},

    # A list of positional arguments.
    [Parameter(ParameterSetName='Command',ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$true)]
    [Parameter(ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$true)]
    [PSObject[]]
    $ArgumentList = @(),

    # A hashtable of techniques to compare
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='Technique',ValueFromPipelineByPropertyName=$true)]
    [Alias('Techniques')]
    [Collections.IDictionary]
    $Technique = @{},

    # The number of times to repeat the benchmark.
    [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
    [Alias('Reps', 'Repetitions', 'RepetitionCount', 'Repeat')]
    [Uint32]$RepeatCount = 100,

    # If set, will provide detailed output, containing average runtimes, minimums, and maximums.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $Detailed,

    # The name of the file that contains the benchmark (excluding the extension).
    # This is used for reporting, and will be automatically populated by Get-Benchmark.
    [Parameter(Position=2,ParameterSetName='Command',ValueFromPipelineByPropertyName=$true)]
    [Parameter(Position=2,ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName=$true)]
    [Parameter(Position=2,ParameterSetName='Technique',ValueFromPipelineByPropertyName=$true)]
    [string]
    $FileName,

    # The name of a logical group. This is used for reporting.
    [Parameter(ParameterSetName='Command',ValueFromPipelineByPropertyName=$true)]
    [Parameter(ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName=$true)]
    [Parameter(ParameterSetName='Technique',ValueFromPipelineByPropertyName=$true)]
    [string]
    $GroupName,

    # If set, will launch as a job.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $AsJob
    )

    begin {
        $cpuSpeed = 
            if ($executionContext.SessionState.PSVariable.Get('IsLinux').Value) {
                Get-Content /proc/cpuinfo -Raw -ErrorAction SilentlyContinue | 
                    Select-String "(?<Unit>Mhz|MIPS)\s+\:\s+(?<Value>[\d\.]+)" | 
                    Select-Object -First 1 -ExpandProperty Matches |
                    ForEach-Object {
                        $_.Groups["Value"].Value -as [int]
                    }
            } elseif ($executionContext.SessionState.PSVariable.Get('IsMacOS').Value) {
                (sysctl -n hw.cpufrequency) / 1e6 -as [int]
            } else {
                $getCimInstance = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Get-CimInstance','Cmdlet')
                if ($getCimInstance) {
                    & $getCimInstance -Class Win32_Processor |
                        Select-Object -ExpandProperty MaxClockSpeed
                }
            }
    }

    process {
        $_splat = @{} + $PSBoundParameters # Before we do anything, make a copy of psboundparameters.

        if ($AsJob) {
            $myself = $MyInvocation.MyCommand
            $_splat.Remove('AsJob')
            Start-Job ([ScriptBlock]::Create("param([Hashtable]`$splat)
function $($myself.Name) {
$($myself.Definition)
}
$($myself.Name) @splat"
)) -ArgumentList $_splat
            return
        }

        #region Resolve the Command
        if ($PSCmdlet.ParameterSetName -eq 'Command') { # If we're benchmarking a command
            # attempt to find the command
            $measureCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand($Command, 'All')
            if (-not $measureCommand) { # If we couldn't find the command
                $commandAsScriptBlock = [ScriptBlock]::Create($Command) # try treating it as a script block.
                if (-not $commandAsScriptBlock) { # If that didn't work
                    Write-Error "Could not resolve $Command" # write an error
                    return # and return.
                }
                # If it was castable to a script block, that's what we're measuring.
                $measureCommand = $commandAsScriptBlock
            }
        }
        #endregion Resolve the Command

        if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { # If we're benchmarking a script block,
            $measureCommand = $ScriptBlock # life is easy (just set $MeasureCommand).
        }

        if (-not $FileName) { # If a file name wasn't provided
            $caller = $(Get-PSCallStack | # peek thru the call stack
                Select-Object -Skip 1 -ExpandProperty InvocationInfo | # (skip yourself)
                Where-Object -Property MyCommand -is ([Management.Automation.ExternalScriptInfo]) | # find external scripts
                Select-Object -First 1 -ExpandProperty MyCommand) # and pull out the first one
            $FileName = # Extract the file name out
                if ($caller.Name -like '*.benchmark.*') {
                    $caller.Name -replace '\.benchmark\.ps1$' -replace '_', ' '
                } elseif ($caller) {
                    $caller.Name -replace '\.ps1$', '' -replace '_', ' '
                }
            if ($FileName) { # If we have a file name, put it in $_splat
                $_splat.FileName = $FileName
            }
        }

        # If we're benchmarking a command or script, let's get to it.
        if ('Command', 'ScriptBlock' -contains $PSCmdlet.ParameterSetName) {
            if ($Detailed) { # If we're asking for detailed benchmarks
                @(foreach ($n in 1..$RepeatCount) { # Measure each execution
                    Measure-Command {
                        & $measureCommand @Parameter @ArgumentList
                    }
                }) |
                # Then determine average, total, min, and max
                Measure-Object -Property TotalMilliseconds -Sum -Average -Minimum -Maximum |
                & { process {
                    # and output a property bag with this information
                    New-Object PSObject -Property @{
                        'Command' = $measureCommand
                        'RepeatCount' = $RepeatCount
                        'TotalMilliseconds' = $_.Sum
                        'TotalTime' = [TimeSpan]::FromMilliseconds($_.Sum)
                        'AverageTime' = [Timespan]::FromMilliseconds($_.Average)
                        'FastestTime' = [Timespan]::FromMilliseconds($_.Minimum)
                        'SlowestTime' = [Timespan]::FromMilliseconds($_.Maximum)
                        FileName = $FileName
                        GroupName = $GroupName
                        BenchmarkInput = $_splat
                        PSTypeName = 'Benchmark.Detail'
                    }
                }}
            } else {
                # If we don't need details, just Measure-Command running it $RepeatCount times
                Measure-Command {
                    foreach ($_ in 1..$RepeatCount) {
                        & $measureCommand @Parameter @ArgumentList
                    }
                } |
                & {
                    process {
                        $_.pstypenames.add('Benchmark.Measure')
                        $_
                    }
                } |
                    Add-Member NoteProperty FileName $FileName -Force -PassThru |
                    Add-Member NoteProperty GroupName $GroupName -Force -PassThru |
                    Add-Member NoteProperty BenchmarkInput $_splat -Force -PassThru
            }
            return # and we're done.
        }


        if ($PSCmdlet.ParameterSetName -eq 'Technique') { # If we're comparing techniques
            $speeds = [Ordered]@{} # Create an ordered hashtable (to keep the speeds)
            $id= Get-Random # get our progress bars ready
            $c, $t = 0, $technique.Count
            $_splat.Remove('Technique') # and remove technique from splat (so we don't infinitely recurse).

            foreach ($techniqueName in @($technique.Keys)) { # Walk over each technique
                $p = $c* 100 / $t
                if ($Technique[$techniqueName] -is [string]) { # If it's a string, try to make it a script block
                    $Technique[$techniqueName] = [ScriptBlock]::Create($technique[$techniqueName])
                    if (-not $Technique[$techniqueName]) { continue } # If that didn't work, keep moving.
                }
                # Call Measure-Benchmark for each technique
                Write-Progress "Timing $techniqueName" "Over $RepeatCount repetitions" -PercentComplete $p -Id $id
                $speeds[$techniqueName] = Measure-Benchmark -ScriptBlock $Technique[$techniqueName] @_splat

                $c++
            }
            Write-Progress "Timing" "Complete" -Completed -Id $id

            # Find the lowest speed
            $lowestSpeed =
                if ($Detailed) {
                    $speeds.GetEnumerator() |
                        Sort-Object { $_.Value.TotalMilliseconds } |
                        Select-Object -First 1
                } else {
                    $speeds.GetEnumerator() |
                        Sort-Object Value |
                        Select-Object -First 1
                }

            # then use that to determine relative speeds.
            $baseFactor = $lowestSpeed.Value.TotalMilliseconds
            $relativeSpeed = [Ordered]@{}
            foreach ($key in $speeds.Keys) {
                $relativeSpeed[$key] = $speeds[$key].TotalMilliseconds / $baseFactor
            }

            # After that's done, sort the speeds from fastest to slowest.
            $sortedSpeeds=
                if ($Detailed) {
                    $speeds.GetEnumerator() |
                        Sort-Object { $_.Value.TotalMilliseconds }
                } else {
                    $speeds.GetEnumerator() | Sort-Object Value
                }


            # Now walk thru each speed from fastest to slowest
            foreach ($kv in $sortedSpeeds) {
                # Create a property bag containing our results
                $ReportItem = [Ordered]@{}
                $ReportItem["Technique"] = $kv.Key
                $ReportItem["Time"] =
                    if ($kv.Value -is [Timespan]) {
                        $kv.Value
                    } else {
                        $kv.Value.TotalTime
                    }
                $ReportItem["RelativeSpeed"] = $relativeSpeed[$kv.Key]
                if ($GroupName) {
                    $ReportItem["GroupName"] = $GroupName
                }
                if ($cpuSpeed) {
                    $ReportItem["ClockSpeed"] = $cpuSpeed
                }
                if ($FileName) {
                    $ReportItem["FileName"] = $FileName
                }
                if ($Detailed) {
                    $ReportItem["Details"] = $kv.Value
                }
                $reportItem["Throughput"] = $repeatCount / $reportItem["Time"].TotalSeconds
                $reportItem["BenchmarkInput"] = $_splat
                $ReportItem["PSTypeName"] = "Benchmark.Relative.Summary"

                # and output it.
                New-Object PSObject -Property $ReportItem
            }
        }
    }
}

Set-Alias bench Measure-Benchmark