e538c0c037
- Add -StaticIP/-Netmask/-Gateway params; injects guestinfo into cloned VMX before start, mirroring job.py _inject_guestinfo_ip behaviour - Add -GuestInfoOnly switch to Get-GuestIPAddress: skips vmrun getGuestIPAddress fallback to prevent race where transient DHCP address is returned before ci-static-ip.ps1 finishes reconfiguring the NIC - Benchmark passes -GuestInfoOnly automatically when -StaticIP is set - staticIP field added to benchmark.jsonl for DHCP vs static comparison Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
335 lines
13 KiB
PowerShell
335 lines
13 KiB
PowerShell
#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"
|
|
|