#Requires -Version 5.1 <# .SYNOPSIS Measures CI infrastructure phase timings: clone, start, IP acquire, WinRM ready, destroy. .DESCRIPTION Creates one or more ephemeral VMs from the template, times each phase, destroys them, then prints a summary table. Results are appended to F:\CI\Logs\benchmark.jsonl for long-term trend tracking (compare before/after §3.1 cache, §3.2 7-Zip, etc.). Phases measured: clone — vmrun clone (linked clone from snapshot) start — vmrun start (until command returns) ip — vmrun getGuestIPAddress (polling until IP appears) winrm — TCP 5986 reachable (polling) destroy — stop + deleteVM + dir removal No actual build is run. Phase 5 build timings come from invoke-ci.jsonl logs produced by Invoke-CIJob.ps1. .PARAMETER TemplatePath Full path to the template VM's .vmx file. Default: F:\CI\Templates\WinBuild2025\WinBuild2025.vmx .PARAMETER SnapshotName Snapshot name to clone from. Default: BaseClean .PARAMETER CloneBaseDir Directory for ephemeral clone folders. Default: F:\CI\BuildVMs .PARAMETER VmrunPath Path to vmrun.exe. Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe .PARAMETER Iterations Number of clone→ready→destroy cycles to run. Multiple iterations reduce timing variance from cold-cache effects. Default: 1 .PARAMETER TimeoutSeconds Maximum seconds to wait for IP and WinRM per iteration. Default: 300 .PARAMETER OutputDir Directory where benchmark.jsonl is appended. Default: F:\CI\Logs .EXAMPLE # Single baseline run .\Measure-CIBenchmark.ps1 # 3 iterations for average (first may be slow due to cold host disk cache) .\Measure-CIBenchmark.ps1 -Iterations 3 # Custom snapshot .\Measure-CIBenchmark.ps1 -SnapshotName BaseClean_20260510 #> [CmdletBinding()] param( [string] $TemplatePath = 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx', [string] $SnapshotName = 'BaseClean', [string] $CloneBaseDir = 'F:\CI\BuildVMs', [string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', [ValidateRange(1, 10)] [int] $Iterations = 1, [ValidateRange(60, 600)] [int] $TimeoutSeconds = 300, [string] $OutputDir = 'F:\CI\Logs' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force Resolve-VmrunPath -VmrunPath $VmrunPath | Out-Null if (-not (Test-Path $TemplatePath -PathType Leaf)) { throw "Template VMX not found: $TemplatePath" } if (-not (Test-Path $CloneBaseDir)) { New-Item -ItemType Directory -Path $CloneBaseDir -Force | Out-Null } if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $benchmarkLog = Join-Path $OutputDir 'benchmark.jsonl' $results = [System.Collections.Generic.List[pscustomobject]]::new() Write-Host "" Write-Host "=== CI Benchmark template: $(Split-Path $TemplatePath -Leaf) snapshot: $SnapshotName iterations: $Iterations ===" Write-Host "" for ($i = 1; $i -le $Iterations; $i++) { Write-Host "--- Iteration $i / $Iterations ---" $runId = "bench-$(Get-Date -Format 'yyyyMMdd-HHmmss')-$i" $cloneVmx = $null $cloneDir = $null $t = [ordered]@{ clone = $null start = $null ip = $null winrm = $null destroy = $null vmIP = $null error = $null } try { # ── Phase: clone ────────────────────────────────────────────────────── $cloneName = "Clone_${runId}" $cloneDir = Join-Path $CloneBaseDir $cloneName $cloneVmx = Join-Path $cloneDir "$cloneName.vmx" Write-Host "[bench] Cloning..." $sw = [System.Diagnostics.Stopwatch]::StartNew() $r = Invoke-Vmrun -VmrunPath $VmrunPath -Operation clone ` -Arguments @($TemplatePath, $cloneVmx, 'linked', '-snapshot', $SnapshotName) ` -ThrowOnError $t.clone = [math]::Round($sw.Elapsed.TotalSeconds, 2) Write-Host "[bench] clone: $($t.clone)s" # ── Phase: start ────────────────────────────────────────────────────── Write-Host "[bench] Starting VM..." $sw.Restart() Invoke-Vmrun -VmrunPath $VmrunPath -Operation start ` -Arguments @($cloneVmx, 'nogui') -ThrowOnError | Out-Null $t.start = [math]::Round($sw.Elapsed.TotalSeconds, 2) Write-Host "[bench] start: $($t.start)s" # ── Phase: IP acquire (poll getGuestIPAddress) ──────────────────────── Write-Host "[bench] Waiting for guest IP..." $sw.Restart() $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $vmIP = $null while ((Get-Date) -lt $deadline) { $ipResult = Invoke-Vmrun -VmrunPath $VmrunPath -Operation getGuestIPAddress ` -Arguments @($cloneVmx) if ($ipResult.ExitCode -eq 0) { $candidate = "$($ipResult.Output)".Trim() if ($candidate -match '^\d+\.\d+\.\d+\.\d+$') { $vmIP = $candidate break } } Start-Sleep -Seconds 3 } if (-not $vmIP) { throw "Timed out waiting for guest IP after $TimeoutSeconds s" } $t.ip = [math]::Round($sw.Elapsed.TotalSeconds, 2) $t.vmIP = $vmIP Write-Host "[bench] ip ($vmIP): $($t.ip)s" # ── Phase: WinRM ready (poll TCP 5986) ──────────────────────────────── Write-Host "[bench] Waiting for WinRM (TCP 5986)..." $sw.Restart() $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $winrmUp = $false while ((Get-Date) -lt $deadline) { $winrmUp = Test-NetConnection -ComputerName $vmIP -Port 5986 ` -InformationLevel Quiet -WarningAction SilentlyContinue ` -ErrorAction SilentlyContinue if ($winrmUp) { break } Start-Sleep -Seconds 3 } if (-not $winrmUp) { throw "Timed out waiting for WinRM on $vmIP`:5986 after $TimeoutSeconds s" } $t.winrm = [math]::Round($sw.Elapsed.TotalSeconds, 2) Write-Host "[bench] winrm: $($t.winrm)s" } catch { $t.error = "$_" Write-Warning "[bench] Iteration $i failed: $_" } finally { # ── Phase: destroy ──────────────────────────────────────────────────── if ($cloneVmx -and (Test-Path $cloneVmx -PathType Leaf)) { Write-Host "[bench] Destroying clone..." $sw = [System.Diagnostics.Stopwatch]::StartNew() & (Join-Path $PSScriptRoot 'Remove-BuildVM.ps1') ` -VMPath $cloneVmx -VmrunPath $VmrunPath -GracefulTimeoutSeconds 10 ` -ErrorAction SilentlyContinue $t.destroy = [math]::Round($sw.Elapsed.TotalSeconds, 2) Write-Host "[bench] destroy: $($t.destroy)s" } elseif ($cloneDir -and (Test-Path $cloneDir)) { Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue } } $totalBootSec = if ($t.ip -and $t.winrm) { [math]::Round($t.clone + $t.start + $t.ip + $t.winrm, 2) } else { $null } $result = [pscustomobject]@{ ts = (Get-Date -Format 'o') runId = $runId iteration = $i template = Split-Path $TemplatePath -Leaf snapshot = $SnapshotName cloneSec = $t.clone startSec = $t.start ipSec = $t.ip winrmSec = $t.winrm destroySec = $t.destroy totalBootSec = $totalBootSec vmIP = $t.vmIP error = $t.error } $results.Add($result) # Append to JSONL log try { $result | ConvertTo-Json -Compress -Depth 3 | Add-Content -Path $benchmarkLog -Encoding UTF8 -ErrorAction Stop } catch { Write-Warning "[bench] Could not write benchmark.jsonl: $_" } Write-Host "" } # ── Summary table ───────────────────────────────────────────────────────────── Write-Host "=== Results ===" $results | Format-Table @( @{ L = 'Iter'; E = { $_.iteration } } @{ L = 'Clone(s)'; E = { $_.cloneSec } } @{ L = 'Start(s)'; E = { $_.startSec } } @{ L = 'IP(s)'; E = { $_.ipSec } } @{ L = 'WinRM(s)'; E = { $_.winrmSec } } @{ L = 'Destroy(s)'; E = { $_.destroySec } } @{ L = 'Boot total'; E = { $_.totalBootSec } } @{ L = 'IP'; E = { $_.vmIP } } @{ L = 'Error'; E = { $_.error } } ) -AutoSize if ($Iterations -gt 1) { $ok = @($results | Where-Object { -not $_.error }) if ($ok.Count -gt 0) { $avgBoot = [math]::Round(($ok | ForEach-Object { $_.totalBootSec } | Measure-Object -Average).Average, 2) Write-Host "Average boot-to-WinRM ($($ok.Count) successful runs): $avgBoot s" } } Write-Host "" Write-Host "Results appended to: $benchmarkLog"