#Requires -Version 5.1 <# .SYNOPSIS Measures CI infrastructure phase timings: clone, start, IP acquire, transport 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 benchmark.jsonl for long-term trend tracking. Phases measured: clone — vmrun clone (linked clone from snapshot) start — vmrun start (until command returns) ip — vmrun getGuestIPAddress (polling until IP appears) ready — TCP port reachable: 5986 (WinRM, Windows) or 22 (SSH, Linux) destroy — stop + deleteVM + dir removal No actual build is run. .PARAMETER TemplatePath Full path to the template VM's .vmx file. .PARAMETER SnapshotName Snapshot name to clone from. Default: BaseClean .PARAMETER CloneBaseDir Directory for ephemeral clone folders. .PARAMETER VmrunPath Path to vmrun executable. .PARAMETER GuestOS 'Windows', 'Linux', or 'Auto' (default). Auto reads guestOS from the VMX: ubuntu-* → Linux (probe SSH/22); anything else → Windows (probe WinRM/5986). .PARAMETER Iterations Number of clone→ready→destroy cycles to run. Default: 1 .PARAMETER TimeoutSeconds Maximum seconds to wait for IP and transport per iteration. Default: 300 .PARAMETER OutputDir Directory where benchmark.jsonl is appended. .EXAMPLE # Single baseline run (Linux host, Windows template) .\Measure-CIBenchmark.ps1 # 4 iterations, Linux guest template .\Measure-CIBenchmark.ps1 -TemplatePath /var/lib/ci/templates/LinuxBuild2404/LinuxBuild2404.vmx ` -SnapshotName BaseClean-Linux -Iterations 4 # Windows host .\Measure-CIBenchmark.ps1 -TemplatePath 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' ` -Iterations 3 #> [CmdletBinding()] param( [string] $TemplatePath = $(if ($null -ne $IsWindows -and $IsWindows -eq $false) { '/var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx' } else { 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' }), [string] $SnapshotName = 'BaseClean', [string] $CloneBaseDir = $(if ($null -ne $IsWindows -and $IsWindows -eq $false) { '/var/lib/ci/build-vms/' } else { 'F:\CI\BuildVMs' }), [string] $VmrunPath = $(if ($null -ne $IsWindows -and $IsWindows -eq $false) { '/usr/bin/vmrun' } else { 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' }), [ValidateSet('Auto', 'Windows', 'Linux')] [string] $GuestOS = 'Auto', [ValidateRange(1, 10)] [int] $Iterations = 1, [ValidateRange(60, 600)] [int] $TimeoutSeconds = 300, [string] $OutputDir = $(if ($null -ne $IsWindows -and $IsWindows -eq $false) { '/var/lib/ci/logs/' } else { 'F:\CI\Logs' }), # Optional static IP injection (mirrors ip_pool in job.py). # If supplied, guestinfo.ip-assignment/netmask/gateway are written into the # cloned VMX before start so ci-static-ip.ps1 applies the IP at boot. [string] $StaticIP = '', [string] $Netmask = '255.255.255.0', [string] $Gateway = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force Resolve-VmrunPath -VmrunPath $VmrunPath | Out-Null # Cross-platform TCP port probe (replaces Test-NetConnection which is Windows-only). function Test-TcpPort { param([string]$ComputerName, [int]$Port, [int]$TimeoutMs = 3000) try { $tcp = [System.Net.Sockets.TcpClient]::new() $ar = $tcp.BeginConnect($ComputerName, $Port, $null, $null) $ok = $ar.AsyncWaitHandle.WaitOne($TimeoutMs, $false) $connected = $ok -and $tcp.Connected $tcp.Close() return $connected } catch { return $false } } # Resolve effective GuestOS from VMX when Auto. $resolvedGuestOS = $GuestOS if ($resolvedGuestOS -eq 'Auto') { $vmxRaw = Get-Content -LiteralPath $TemplatePath -Raw -ErrorAction SilentlyContinue if ($vmxRaw -match 'guestOS\s*=\s*"([^"]+)"' -and $Matches[1] -like 'ubuntu*') { $resolvedGuestOS = 'Linux' } else { $resolvedGuestOS = 'Windows' } Write-Host "[bench] GuestOS auto-detected: $resolvedGuestOS" } $transportPort = if ($resolvedGuestOS -eq 'Linux') { 22 } else { 5986 } $transportLabel = if ($resolvedGuestOS -eq 'Linux') { 'SSH/22' } else { 'WinRM/5986' } 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 } function Set-GuestInfoIP { param([string]$VmxPath, [string]$IP, [string]$Mask, [string]$Gw) $keys = @('guestinfo.ip-assignment', 'guestinfo.netmask', 'guestinfo.gateway') $lines = (Get-Content -LiteralPath $VmxPath -Encoding UTF8) | Where-Object { ($_ -split '=')[0].Trim().ToLower() -notin $keys } $lines += "guestinfo.ip-assignment = `"$IP`"" if ($Mask) { $lines += "guestinfo.netmask = `"$Mask`"" } if ($Gw) { $lines += "guestinfo.gateway = `"$Gw`"" } Set-Content -LiteralPath $VmxPath -Value $lines -Encoding UTF8 } $benchmarkLog = Join-Path $OutputDir 'benchmark.jsonl' $results = [System.Collections.Generic.List[pscustomobject]]::new() $useStaticIP = $StaticIP -match '^\d{1,3}(\.\d{1,3}){3}$' if ($useStaticIP) { Write-Host "[bench] Static IP mode: $StaticIP / $Netmask gw=$Gateway" } else { Write-Host "[bench] DHCP mode (no -StaticIP supplied)" } 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 ready = $null destroy = $null deltaKB = $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() $null = 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" # Measure linked-clone disk footprint try { $cloneSizeBytes = (Get-ChildItem -Path $cloneDir -Recurse -File -ErrorAction Stop | Measure-Object -Property Length -Sum).Sum $t.deltaKB = [math]::Round($cloneSizeBytes / 1KB, 0) Write-Host "[bench] clone size: $($t.deltaKB) KB" } catch { Write-Warning "[bench] Could not measure clone size: $_" } # ── Static IP injection (before start) ─────────────────────────────── if ($useStaticIP) { Set-GuestInfoIP -VmxPath $cloneVmx -IP $StaticIP -Mask $Netmask -Gw $Gateway Write-Host "[bench] guestinfo injected: $StaticIP" } # ── 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 (primary: guestinfo.ci-ip, fallback: getGuestIPAddress) ── Write-Host "[bench] Waiting for guest IP..." $sw.Restart() $vmIP = Get-GuestIPAddress -VmrunPath $VmrunPath -VmxPath $cloneVmx ` -TimeoutSeconds $TimeoutSeconds -GuestInfoOnly:$useStaticIP 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: transport ready (SSH/22 for Linux, WinRM/5986 for Windows) ── Write-Host "[bench] Waiting for $transportLabel..." $sw.Restart() $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $transportUp = $false while ((Get-Date) -lt $deadline) { $transportUp = Test-TcpPort -ComputerName $vmIP -Port $transportPort if ($transportUp) { break } Start-Sleep -Seconds 3 } if (-not $transportUp) { throw "Timed out waiting for $transportLabel on $vmIP after $TimeoutSeconds s" } $t.ready = [math]::Round($sw.Elapsed.TotalSeconds, 2) Write-Host "[bench] ready ($transportLabel): $($t.ready)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 ($null -ne $t.ip -and $null -ne $t.ready) { [math]::Round($t.clone + $t.start + $t.ip + $t.ready, 2) } else { $null } $result = [pscustomobject]@{ ts = (Get-Date -Format 'o') runId = $runId iteration = $i template = Split-Path $TemplatePath -Leaf snapshot = $SnapshotName guestOS = $resolvedGuestOS staticIP = if ($useStaticIP) { $StaticIP } else { $null } cloneSec = $t.clone startSec = $t.start ipSec = $t.ip readySec = $t.ready destroySec = $t.destroy totalBootSec = $totalBootSec deltaKB = $t.deltaKB 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 = 'Ready(s)'; E = { $_.readySec } } @{ L = 'Destroy(s)'; E = { $_.destroySec } } @{ L = 'Boot total'; E = { $_.totalBootSec } } @{ L = 'Delta(KB)'; E = { $_.deltaKB } } @{ 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-ready ($($ok.Count) successful runs): $avgBoot s" } } Write-Host "" Write-Host "Results appended to: $benchmarkLog"