feat(sprint4-6): quality, reliability, perf baseline
Sprint 4 — qualità codice: - scripts/_Common.psm1: New-CISessionOption, Resolve-VmrunPath, Invoke-Vmrun - PSScriptAnalyzerSettings.psd1: project-wide PSSA rules (security + formatting) - Remove-BuildVM.ps1: SupportsShouldProcess (-WhatIf support) - Invoke-RemoteBuild.ps1, Get-BuildArtifacts.ps1: use New-CISessionOption from module - New-BuildVM.ps1: use Resolve-VmrunPath + Invoke-Vmrun from module Sprint 5 — affidabilità: - Backup-CITemplate.ps1: stop runner, timestamped VMDK backup, prune old, restart - Watch-RunnerHealth.ps1: auto-restart act_runner, cooldown 3/h, EventLog + webhook - Register-CIScheduledTasks.ps1: add CI-RunnerHealth task (every 15 min) Sprint 6 — perf baseline: - Set-TemplateSharedFolders.ps1: idempotent VMX editor for NuGet/pip shared folders - Invoke-RemoteBuild.ps1: -UseSharedCache injects NUGET_PACKAGES + PIP_CACHE_DIR in guest - Measure-CIBenchmark.ps1: times clone/start/IP/WinRM/destroy, appends to benchmark.jsonl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
#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\WinBuild\CI-WinBuild.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\WinBuild\CI-WinBuild.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'
|
||||
|
||||
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
|
||||
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"
|
||||
|
||||
# ── 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 (poll getGuestIPAddress) ────────────────────────
|
||||
Write-Host "[bench] Waiting for guest IP..."
|
||||
$sw.Restart()
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$vmIP = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$ipResult = Invoke-Vmrun -VmrunPath $VmrunPath -Operation getGuestIPAddress `
|
||||
-Arguments @($cloneVmx)
|
||||
if ($ipResult.ExitCode -eq 0) {
|
||||
$candidate = "$($ipResult.Output)".Trim()
|
||||
if ($candidate -match '^\d+\.\d+\.\d+\.\d+$') {
|
||||
$vmIP = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
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
|
||||
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 = '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"
|
||||
Reference in New Issue
Block a user