From 4f9149f7e29251c212c4084c662df16381372e20 Mon Sep 17 00:00:00 2001 From: Simone Date: Sat, 23 May 2026 23:57:32 +0200 Subject: [PATCH] fix(benchmark): adapt Measure-CIBenchmark to Linux host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/Measure-CIBenchmark.ps1 | 146 ++++++++++++++++++++++---------- scripts/_Common.psm1 | 7 +- 2 files changed, 106 insertions(+), 47 deletions(-) diff --git a/scripts/Measure-CIBenchmark.ps1 b/scripts/Measure-CIBenchmark.ps1 index 66bc05b..2d95084 100644 --- a/scripts/Measure-CIBenchmark.ps1 +++ b/scripts/Measure-CIBenchmark.ps1 @@ -1,63 +1,83 @@ #Requires -Version 5.1 <# .SYNOPSIS - Measures CI infrastructure phase timings: clone, start, IP acquire, WinRM ready, destroy. + 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 F:\CI\Logs\benchmark.jsonl - for long-term trend tracking (compare before/after §3.1 cache, §3.2 7-Zip, etc.). + 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) - winrm — TCP 5986 reachable (polling) - destroy — stop + deleteVM + dir removal + 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. Phase 5 build timings come from invoke-ci.jsonl logs - produced by Invoke-CIJob.ps1. + No actual build is run. .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 + Directory for ephemeral clone folders. .PARAMETER VmrunPath - Path to vmrun.exe. - Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe + 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. Multiple iterations reduce timing - variance from cold-cache effects. Default: 1 + Number of clone→ready→destroy cycles to run. Default: 1 .PARAMETER TimeoutSeconds - Maximum seconds to wait for IP and WinRM per iteration. Default: 300 + Maximum seconds to wait for IP and transport per iteration. Default: 300 .PARAMETER OutputDir - Directory where benchmark.jsonl is appended. Default: F:\CI\Logs + Directory where benchmark.jsonl is appended. .EXAMPLE - # Single baseline run + # Single baseline run (Linux host, Windows template) .\Measure-CIBenchmark.ps1 - # 3 iterations for average (first may be slow due to cold host disk cache) - .\Measure-CIBenchmark.ps1 -Iterations 3 + # 4 iterations, Linux guest template + .\Measure-CIBenchmark.ps1 -TemplatePath /var/lib/ci/templates/LinuxBuild2404/LinuxBuild2404.vmx ` + -SnapshotName BaseClean-Linux -Iterations 4 - # Custom snapshot - .\Measure-CIBenchmark.ps1 -SnapshotName BaseClean_20260510 + # Windows host + .\Measure-CIBenchmark.ps1 -TemplatePath 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' ` + -Iterations 3 #> [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', + [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, @@ -65,16 +85,49 @@ param( [ValidateRange(60, 600)] [int] $TimeoutSeconds = 300, - [string] $OutputDir = 'F:\CI\Logs' + [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' # suppress Write-Progress (Test-NetConnection, IWR, etc.) +$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" } @@ -102,7 +155,7 @@ for ($i = 1; $i -le $Iterations; $i++) { clone = $null start = $null ip = $null - winrm = $null + ready = $null destroy = $null deltaKB = $null vmIP = $null @@ -151,21 +204,21 @@ for ($i = 1; $i -le $Iterations; $i++) { $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)..." + # ── Phase: transport ready (SSH/22 for Linux, WinRM/5986 for Windows) ── + Write-Host "[bench] Waiting for $transportLabel..." $sw.Restart() - $deadline = (Get-Date).AddSeconds($TimeoutSeconds) - $winrmUp = $false + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $transportUp = $false while ((Get-Date) -lt $deadline) { - $winrmUp = Test-NetConnection -ComputerName $vmIP -Port 5986 ` - -InformationLevel Quiet -WarningAction SilentlyContinue ` - -ErrorAction SilentlyContinue - if ($winrmUp) { break } + $transportUp = Test-TcpPort -ComputerName $vmIP -Port $transportPort + if ($transportUp) { 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" + 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 = "$_" @@ -186,8 +239,8 @@ for ($i = 1; $i -le $Iterations; $i++) { } } - $totalBootSec = if ($t.ip -and $t.winrm) { - [math]::Round($t.clone + $t.start + $t.ip + $t.winrm, 2) + $totalBootSec = if ($t.ip -and $t.ready) { + [math]::Round($t.clone + $t.start + $t.ip + $t.ready, 2) } else { $null } $result = [pscustomobject]@{ @@ -196,10 +249,11 @@ for ($i = 1; $i -le $Iterations; $i++) { iteration = $i template = Split-Path $TemplatePath -Leaf snapshot = $SnapshotName + guestOS = $resolvedGuestOS cloneSec = $t.clone startSec = $t.start ipSec = $t.ip - winrmSec = $t.winrm + readySec = $t.ready destroySec = $t.destroy totalBootSec = $totalBootSec deltaKB = $t.deltaKB @@ -226,7 +280,7 @@ $results | Format-Table @( @{ L = 'Clone(s)'; E = { $_.cloneSec } } @{ L = 'Start(s)'; E = { $_.startSec } } @{ L = 'IP(s)'; E = { $_.ipSec } } - @{ L = 'WinRM(s)'; E = { $_.winrmSec } } + @{ L = 'Ready(s)'; E = { $_.readySec } } @{ L = 'Destroy(s)'; E = { $_.destroySec } } @{ L = 'Boot total'; E = { $_.totalBootSec } } @{ L = 'Delta(KB)'; E = { $_.deltaKB } } @@ -239,7 +293,7 @@ if ($Iterations -gt 1) { 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 "Average boot-to-ready ($($ok.Count) successful runs): $avgBoot s" } } diff --git a/scripts/_Common.psm1 b/scripts/_Common.psm1 index d849225..db3dc84 100644 --- a/scripts/_Common.psm1 +++ b/scripts/_Common.psm1 @@ -182,7 +182,12 @@ function Invoke-VmrunBounded { # handles held by child processes (e.g. cmd.exe spawning ping.exe in tests, # or vmrun spawning helper subprocesses). Plain proc.Kill() only kills the # direct process; child processes keep the write end of stdout open. - & taskkill /F /T /PID $proc.Id 2>&1 | Out-Null + if ($null -ne $IsWindows -and $IsWindows -eq $false) { + # .NET 5+ Kill(entireProcessTree) — works on Linux/macOS. + $proc.Kill($true) + } else { + & taskkill /F /T /PID $proc.Id 2>&1 | Out-Null + } } $sw.Stop()