6cd6bff882
- 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
249 lines
9.6 KiB
PowerShell
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"
|
|
|