Invoke-Script.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
function Invoke-Script {
    <#
    .DESCRIPTION
    Invoke the provided script or command and measure the time it takes to execute it.
 
    .PARAMETER ScriptBlock
    A ScriptBlock to be executed.
 
    $trace = Invoke-Script -ScriptBlock { Import-Module $PSScriptRoot\Planets.psm1; Get-Planet } -Repeat 3
 
    When running a script file use the same syntax, you can specify parameters if you need:
    $trace = Invoke-Script -ScriptBlock { & $PSScriptRoot\MyScript.ps1 } -Repeat 3
 
    .PARAMETER Repeat
    Run the provided script or command this many times in the same session. Default is 1.
    The first two runs are usually much slower than the subsequent runs. Using 3 or more
    is good to get the idea of overall performance, but if you are optimizing for fast single
    run you might want to keep 1.
 
    .PARAMETER Preheat
    Run the provided script or command this many times without tracing it to warm up the session.
    This is good for measuring normal performance excluding startup. Default is 0.
 
    .PARAMETER Flag
    A hash table of feature flags to be enabled to do before/after comparisons. The script or command will run
    twice in each repetition. Once as before changes and once as after changes.
 
    For this to work your code needs to cooperate:
 
    Each key from the hashtable will be defined as a $global: variable. Use that variable in `if` to wrap
    the new code to test it against the old code.
 
    Use $true, $false as the values. The hashtable represents the after side. Each key will be
    defined with the value for after side. Each key will be defined with $false for the before side.
 
    In this case example is worth 1000 words:
 
    Invoke-Script -ScriptBlock { & MyScript.ps1 } -Flag @{ _profiler = $true }
 
    Original code in MyScript.ps1 you are trying to improve:
    $values = 1..1000
    foreach ($v in $Values) {
        $newValues += $v + 10
    }
 
    Modified code with changes that you think are faster wrapped in if using the feature flags:
    if ($_profiler) {
        $values = [System.Linq.Enumerable]::Range(1,1000)
    }
    else {
        $values = 1..1000
    }
 
    if ($_profiler) {
        # after side
        $newValues = foreach ($v in $Values) { $v + 10 }
    }
    else {
        # before side
        foreach ($v in $Values) {
            $newValues += $v + 10
        }
    }
 
    A single feature flag might be good enough for many purposes, but imagine there are multiple improvements
    that migh contradict each other, in that case you are better of adding different flag for each one:
 
    Invoke-Script -ScriptBlock { & MyScript.ps1 } -Flag @{ _iteration = $true; _enumerable = $true }
 
    if ($_enumerable) {
        $values = [System.Linq.Enumerable]::Range(1,1000)
    }
    else {
        $values = 1..1000
    }
 
    if ($_iteration) {
        # B side
        $newValues = foreach ($v in $Values) { $v + 10 }
    }
    else {
        # A side
        foreach ($v in $Values) {
            $newValues += $v + 10
        }
    }
 
    Using two or more feature flags you can test them together as shown above, or one without the other:
 
    Invoke-Script -ScriptBlock { & MyScript.ps1 } -Flag @{ _iteration = $true; _enumerable = $false }
    #>


    param(
        [Parameter(Mandatory)]
        [ScriptBlock] $ScriptBlock,
        [uint32] $Repeat = 1,
        [uint32] $Preheat = 0,
        [Hashtable] $Flag
    )

    $ErrorView = "Normal"
    $ErrorActionPreference = "Stop"

    Assert-PowerShellVersion

    Write-Host -ForegroundColor Magenta "Running in PowerShell $($PSVersionTable.PSVersion) $([System.IntPtr]::Size*8)-bit."
    if ($Flag) {
        Write-Host -ForegroundColor Magenta "Flags for After runs:"
        foreach ($f in $Flag.GetEnumerator()) {
            Write-Host -ForegroundColor Magenta " $($f.Key) = $($f.Value)"
        }
    }

    if (0 -ge $Repeat) {
        $Repeat = 1
    }

    if (0 -ge $Preheat) {
        $Preheat = 0
    }

    if (0 -lt $Preheat) { 
        foreach ($i in 1..$Preheat) {
            Write-Host -Foreground Magenta  "Warm up $i"
            if (0 -lt $Flag.Count) { 
                foreach ($p in $Flag.GetEnumerator()) {
                    $v = if ($i % 2 -eq 1) { 
                        $false
                    } 
                    else {
                        $p.Value
                    }

                    Set-Variable -Scope Global -Name $p.Key -Value $v
                }
            }
            $sw = [System.Diagnostics.Stopwatch]::StartNew()
            try {
                $null = & $ScriptBlock
                $sw.Stop()
            }
            catch {
                $sw.Stop()
                Write-Host "Warm up failed with $_."
            }
        }
    }

    $run = [Collections.Generic.List[object]]@()

    foreach ($i in 1..$Repeat) {
        $sides = if ($null -ne $Flag) { "Before", "After" } else { "After" }        
        
        foreach ($side in $sides) {
            Write-Host -Foreground Magenta  "Run $i$(if (1 -lt $sides.Count) { " - $side" })"
            if (0 -lt $Flag.Count) { 
                foreach ($p in $Flag.GetEnumerator()) {
                    $v = if ("Before" -eq $side) { 
                        $false
                    } 
                    else {
                        $p.Value
                    }

                    Set-Variable -Scope Global -Name $p.Key -Value $v
                }
            }
            $err = $null
            $sw = [System.Diagnostics.Stopwatch]::StartNew()
            try {
                $null = & $ScriptBlock
                $sw.Stop()
                Write-Host -Foreground Magenta  "Run $i$(if (1 -lt $sides.Count) { " - $side" }) finished after $($sw.Elapsed)"
            }
            catch { 
                $sw.Stop()
                $err = $_
                Write-Host "Run $i$(if (1 -lt $sides.Count) { " - $side" }) failed after $($sw.Elapsed) with $_."
            }

            $null = $run.Add(@{ Error = $err; SelfDuration = $sw.Elapsed; Side = $side })
        }
    }

    Write-Host -Foreground Magenta  "Done."

    if ($Flag) { 
        for ($r = 0; $r -lt $run.Count; $r = $r + 2) { 
            $b = $run[$r]
            $a = $run[$r + 1]

            $diff = [int]($a.SelfDuration.TotalMilliseconds - $b.SelfDuration.TotalMilliseconds)
            if ($diff -eq 0) { 
                $beforeColor = $afterColor = "Yellow"
            }
            elseif ($diff -gt 0) { 
                $beforeColor = "Green"
                $afterColor = "Red"
            }
            else { 
                $beforeColor = "Red"
                $afterColor = "Green"
            }

            Write-Host -NoNewline "Run $(${r}/2): "
            Write-Host -ForegroundColor $beforeColor $b.SelfDuration -NoNewline 
            Write-Host -NoNewline " -> "
            Write-Host -ForegroundColor $afterColor $a.SelfDuration -NoNewline 
            Write-Host " ($($diff) ms)"
        }
    }
}