Files
local-ci-cd-system/scripts/Measure-CIBenchmark.ps1
T
Simone 6cd6bff882 nice-to-have: implement all 6 tier items
- Get-CIJobSummary.ps1: already present with full implementation (marked done)
- Get-BuildArtifacts.ps1: add -JobId/-Commit params; Write-ArtifactManifest writes
  manifest.json (name, size, sha256, commit, jobId) after both Linux and Windows
  artifact collection branches
- Invoke-CIJob.ps1: add -GuestCPU/-GuestMemoryMB params; insert post-clone VMX
  override block (numvcpus/memsize rewrite) before vmrun start; pass -JobId/-Commit
  to Get-BuildArtifacts calls
- Watch-RunnerHealth.ps1: add -GiteaUrl/-GiteaCredentialTarget params; optional
  GET /api/v1/admin/runners check in Running branch warns if 0 online runners
- Install-CIToolchain-WinBuild2025.ps1: fill SHA256 hashes for Python 3.13.3,
  7-Zip 26.01, Node.js 22.14.0, dotnet-install.ps1
- Measure-CIBenchmark.ps1: add deltaKB field (linked-clone disk footprint in KB)
  measured post-clone; added to result object and summary Format-Table
2026-05-13 10:38:49 +02:00

249 lines
9.6 KiB
PowerShell

#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'
$ProgressPreference = 'SilentlyContinue' # suppress Write-Progress (Test-NetConnection, IWR, etc.)
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
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()
$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"
# 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: 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
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 = 'WinRM(s)'; E = { $_.winrmSec } }
@{ 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-WinRM ($($ok.Count) successful runs): $avgBoot s"
}
}
Write-Host ""
Write-Host "Results appended to: $benchmarkLog"