Files
local-ci-cd-system/scripts/Measure-CIBenchmark.ps1
T
Simone 4f9149f7e2 fix(benchmark): adapt Measure-CIBenchmark to Linux host
Measure-CIBenchmark.ps1:
- Default params now Linux-aware via $IsWindows conditional
  (TemplatePath, CloneBaseDir, VmrunPath, OutputDir)
- Add -GuestOS Auto/Windows/Linux param; Auto reads VMX guestOS field
  (ubuntu-* → Linux, else Windows)
- Replace Test-NetConnection (Windows-only) with Test-TcpPort helper
  using raw TcpClient async connect — works on PS5.1 and PS7/Linux
- Probe SSH/22 for Linux guests, WinRM/5986 for Windows guests
- Rename winrm→ready throughout ($t, JSONL field readySec, column header)
- Add guestOS field to JSONL output

_Common.psm1 / Invoke-VmrunBounded:
- Replace taskkill /F /T with platform-aware kill:
  Windows → taskkill; Linux → Process.Kill(entireProcessTree) (.NET 5+)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 23:57:32 +02:00

303 lines
11 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'
})
)
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
}
$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
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: $_"
}
# ── 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
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 ($t.ip -and $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
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"