|
| 1 | +[CmdletBinding()] |
| 2 | +param ( |
| 3 | + [Parameter()] |
| 4 | + [ValidateSet('Baseline', 'Regression')] |
| 5 | + [string] $Mode = 'Baseline', |
| 6 | + |
| 7 | + [Parameter()] |
| 8 | + [ValidateRange(1, 200)] |
| 9 | + [int] $Runs = 12, |
| 10 | + |
| 11 | + [Parameter()] |
| 12 | + [ValidateRange(0, 50)] |
| 13 | + [int] $Preheat = 2, |
| 14 | + |
| 15 | + [Parameter()] |
| 16 | + [string] $OutputPath = (Join-Path $PSScriptRoot "..\fixtures\perf\$Mode.json") |
| 17 | +) |
| 18 | + |
| 19 | +Set-StrictMode -Version Latest |
| 20 | +$ErrorActionPreference = 'Stop' |
| 21 | + |
| 22 | +if ($PSVersionTable.PSVersion -lt [version] '7.6' -or $PSVersionTable.PSEdition -cne 'Core') { |
| 23 | + throw 'Invoke-YamlPerfReview.ps1 requires PowerShell 7.6+ (Core).' |
| 24 | +} |
| 25 | + |
| 26 | +Import-Module -Name Profiler -ErrorAction Stop |
| 27 | +. (Join-Path $PSScriptRoot '..\TestBootstrap.ps1') |
| 28 | + |
| 29 | +function Get-Percentile { |
| 30 | + [CmdletBinding()] |
| 31 | + param ( |
| 32 | + [Parameter(Mandatory)] |
| 33 | + [double[]] $Values, |
| 34 | + |
| 35 | + [Parameter(Mandatory)] |
| 36 | + [ValidateRange(0.0, 1.0)] |
| 37 | + [double] $Percentile |
| 38 | + ) |
| 39 | + |
| 40 | + if ($Values.Count -eq 0) { |
| 41 | + return 0.0 |
| 42 | + } |
| 43 | + |
| 44 | + $sorted = @($Values | Sort-Object) |
| 45 | + $index = [Math]::Ceiling($Percentile * $sorted.Count) - 1 |
| 46 | + $index = [Math]::Max(0, [Math]::Min($index, $sorted.Count - 1)) |
| 47 | + return [double] $sorted[$index] |
| 48 | +} |
| 49 | + |
| 50 | +function Measure-Scenario { |
| 51 | + [CmdletBinding()] |
| 52 | + param ( |
| 53 | + [Parameter(Mandatory)] |
| 54 | + [string] $Name, |
| 55 | + |
| 56 | + [Parameter(Mandatory)] |
| 57 | + [scriptblock] $Script, |
| 58 | + |
| 59 | + [Parameter(Mandatory)] |
| 60 | + [int] $RunCount, |
| 61 | + |
| 62 | + [Parameter(Mandatory)] |
| 63 | + [int] $WarmupCount |
| 64 | + ) |
| 65 | + |
| 66 | + for ($i = 0; $i -lt $WarmupCount; $i++) { |
| 67 | + & $Script > $null |
| 68 | + } |
| 69 | + |
| 70 | + $elapsedMs = [System.Collections.Generic.List[double]]::new() |
| 71 | + for ($i = 0; $i -lt $RunCount; $i++) { |
| 72 | + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() |
| 73 | + & $Script > $null |
| 74 | + $stopwatch.Stop() |
| 75 | + $elapsedMs.Add($stopwatch.Elapsed.TotalMilliseconds) |
| 76 | + } |
| 77 | + |
| 78 | + $trace = Trace-Script -ScriptBlock $Script -Preheat 1 -DisableWarning |
| 79 | + $topFunctions = @( |
| 80 | + $trace.Top50FunctionSelfDuration | |
| 81 | + Select-Object -First 5 Function, Module, Line, HitCount, @{ |
| 82 | + Name = 'SelfDurationMs' |
| 83 | + Expression = { [Math]::Round($_.SelfDuration.TotalMilliseconds, 3) } |
| 84 | + }, @{ |
| 85 | + Name = 'DurationMs' |
| 86 | + Expression = { [Math]::Round($_.Duration.TotalMilliseconds, 3) } |
| 87 | + } |
| 88 | + ) |
| 89 | + |
| 90 | + $values = [double[]] $elapsedMs.ToArray() |
| 91 | + [pscustomobject]@{ |
| 92 | + Name = $Name |
| 93 | + Runs = $RunCount |
| 94 | + Preheat = $WarmupCount |
| 95 | + AverageMs = [Math]::Round(($values | Measure-Object -Average).Average, 3) |
| 96 | + MedianMs = [Math]::Round((Get-Percentile -Values $values -Percentile 0.50), 3) |
| 97 | + P95Ms = [Math]::Round((Get-Percentile -Values $values -Percentile 0.95), 3) |
| 98 | + MinMs = [Math]::Round(($values | Measure-Object -Minimum).Minimum, 3) |
| 99 | + MaxMs = [Math]::Round(($values | Measure-Object -Maximum).Maximum, 3) |
| 100 | + SamplesMs = $values |
| 101 | + TraceTopSelf = $topFunctions |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +Write-Host "Preparing performance fixtures for mode: $Mode" |
| 106 | + |
| 107 | +$smallYaml = @' |
| 108 | +name: small |
| 109 | +enabled: true |
| 110 | +ports: [80, 443] |
| 111 | +'@ |
| 112 | + |
| 113 | +$mediumYamlBuilder = [System.Text.StringBuilder]::new() |
| 114 | +[void] $mediumYamlBuilder.AppendLine('items:') |
| 115 | +for ($index = 0; $index -lt 1200; $index++) { |
| 116 | + [void] $mediumYamlBuilder.AppendLine(" - id: $index") |
| 117 | + [void] $mediumYamlBuilder.AppendLine(" name: item-$index") |
| 118 | + [void] $mediumYamlBuilder.AppendLine(" enabled: true") |
| 119 | + [void] $mediumYamlBuilder.AppendLine(" value: $($index * 3)") |
| 120 | +} |
| 121 | +$mediumYaml = $mediumYamlBuilder.ToString().TrimEnd("`r", "`n") |
| 122 | + |
| 123 | +$overlayYamlBuilder = [System.Text.StringBuilder]::new() |
| 124 | +[void] $overlayYamlBuilder.AppendLine('items:') |
| 125 | +for ($index = 0; $index -lt 1200; $index++) { |
| 126 | + [void] $overlayYamlBuilder.AppendLine(" - id: $index") |
| 127 | + [void] $overlayYamlBuilder.AppendLine(" name: item-$index-override") |
| 128 | + [void] $overlayYamlBuilder.AppendLine(" enabled: false") |
| 129 | +} |
| 130 | +$overlayYaml = $overlayYamlBuilder.ToString().TrimEnd("`r", "`n") |
| 131 | + |
| 132 | +$mediumObject = ConvertFrom-Yaml -Yaml $mediumYaml -NoEnumerate |
| 133 | +$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) 'yaml-perf-review.yaml' |
| 134 | + |
| 135 | +$scenarios = @( |
| 136 | + @{ |
| 137 | + Name = 'ConvertFrom-Yaml/small' |
| 138 | + Script = { ConvertFrom-Yaml -Yaml $smallYaml | Out-Null } |
| 139 | + }, |
| 140 | + @{ |
| 141 | + Name = 'ConvertFrom-Yaml/medium' |
| 142 | + Script = { ConvertFrom-Yaml -Yaml $mediumYaml -NoEnumerate | Out-Null } |
| 143 | + }, |
| 144 | + @{ |
| 145 | + Name = 'ConvertTo-Yaml/medium' |
| 146 | + Script = { ConvertTo-Yaml -InputObject $mediumObject | Out-Null } |
| 147 | + }, |
| 148 | + @{ |
| 149 | + Name = 'Format-Yaml/medium' |
| 150 | + Script = { Format-Yaml -InputObject $mediumYaml -Indent 2 | Out-Null } |
| 151 | + }, |
| 152 | + @{ |
| 153 | + Name = 'Merge-Yaml/medium' |
| 154 | + Script = { Merge-Yaml -InputObject @($mediumYaml, $overlayYaml) -SequenceAction Replace | Out-Null } |
| 155 | + }, |
| 156 | + @{ |
| 157 | + Name = 'Remove-YamlEntry/medium' |
| 158 | + Script = { Remove-YamlEntry -InputObject $mediumYaml -Path '/items/12/name' -IgnoreMissing | Out-Null } |
| 159 | + }, |
| 160 | + @{ |
| 161 | + Name = 'Test-Yaml/medium' |
| 162 | + Script = { Test-Yaml -Yaml $mediumYaml | Out-Null } |
| 163 | + }, |
| 164 | + @{ |
| 165 | + Name = 'Export-Import-Yaml/medium' |
| 166 | + Script = { |
| 167 | + Export-Yaml -InputObject $mediumObject -Path $tempPath -Force | Out-Null |
| 168 | + Import-Yaml -LiteralPath $tempPath -NoEnumerate | Out-Null |
| 169 | + } |
| 170 | + } |
| 171 | +) |
| 172 | + |
| 173 | +$results = [System.Collections.Generic.List[object]]::new() |
| 174 | +foreach ($scenario in $scenarios) { |
| 175 | + Write-Host "Measuring $($scenario.Name)" |
| 176 | + $results.Add((Measure-Scenario -Name $scenario.Name -Script $scenario.Script -RunCount $Runs -WarmupCount $Preheat)) |
| 177 | +} |
| 178 | + |
| 179 | +if (Test-Path -LiteralPath $tempPath) { |
| 180 | + Remove-Item -LiteralPath $tempPath -Force |
| 181 | +} |
| 182 | + |
| 183 | +$outputDirectory = Split-Path -Parent $OutputPath |
| 184 | +if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -LiteralPath $outputDirectory)) { |
| 185 | + $null = New-Item -Path $outputDirectory -ItemType Directory -Force |
| 186 | +} |
| 187 | + |
| 188 | +$report = [pscustomobject]@{ |
| 189 | + Mode = $Mode |
| 190 | + TimestampUtc = [DateTime]::UtcNow.ToString('o') |
| 191 | + PowerShell = $PSVersionTable.PSVersion.ToString() |
| 192 | + Edition = $PSVersionTable.PSEdition |
| 193 | + Runs = $Runs |
| 194 | + Preheat = $Preheat |
| 195 | + RegressionFail = 'Greater than 5 percent slowdown on critical-path scenarios.' |
| 196 | + Scenarios = $results |
| 197 | +} |
| 198 | + |
| 199 | +$report | ConvertTo-Json -Depth 8 | Set-Content -Path $OutputPath -Encoding utf8NoBOM |
| 200 | +Write-Host "Performance report written to: $OutputPath" |
0 commit comments