ModernUO/tools/Measure-EventLoop.ps1
Kamron Batman e09bd6ea71
tools: event-loop measurement harness
The A/B harness used to measure the idle-sleep work that shipped in
main (#2559): Measure-EventLoop.ps1 and measure-event-loop.sh drive
CPU/lag comparisons across server.eventLoopIdleWaitMs settings, and
HostLatencyProbe.cs measures what basic operations cost on a host.
dev-docs/measuring-event-loop.md explains the method, the numbers that
matter, and how to re-vendor IORingGroup for ring experiments.

This branch is main plus this commit, rebased forward as main moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 13:35:41 -07:00

96 lines
3.4 KiB
PowerShell

# A/B measurement for the event loop scheduler.
#
# Boots the shard twice against identical binaries -- once with idle sleeping disabled
# (server.eventLoopIdleWaitMs = 0) and once with idle sleeping (= 2) -- and samples process CPU
# time over a fixed window. Everything else is held constant, so the delta is the scheduler.
#
# Usage: pwsh tools/Measure-EventLoop.ps1 [-WarmupSeconds 45] [-SampleSeconds 60]
param(
[int]$WarmupSeconds = 45,
[int]$SampleSeconds = 60
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$dist = Join-Path $root 'Distribution'
$exe = Join-Path $dist 'ModernUO.exe'
$configPath = Join-Path $dist 'Configuration\modernuo.json'
if (-not (Test-Path $exe)) {
throw "ModernUO.exe not found at $exe. Build with: dotnet build -c Release"
}
function Set-IdleWait([int]$value) {
$json = Get-Content $configPath -Raw | ConvertFrom-Json
$json.settings.'server.eventLoopIdleWaitMs' = "$value"
$json | ConvertTo-Json -Depth 20 | Set-Content $configPath -Encoding UTF8
}
function Measure-Loop([int]$idleWait, [string]$label) {
Set-IdleWait $idleWait
Write-Host ""
Write-Host "=== $label (server.eventLoopIdleWaitMs = $idleWait) ===" -ForegroundColor Cyan
# Redirect stdin from an empty file so the server runs headless and never blocks on console
# input. PowerShell cannot redirect from NUL, so an actual empty file stands in for it.
$stdin = Join-Path $dist 'empty.in'
if (-not (Test-Path $stdin)) {
Set-Content -Path $stdin -Value '' -NoNewline
}
$proc = Start-Process -FilePath $exe -WorkingDirectory $dist -PassThru `
-RedirectStandardInput $stdin `
-RedirectStandardOutput (Join-Path $dist "Logs\measure-$idleWait.out") `
-RedirectStandardError (Join-Path $dist "Logs\measure-$idleWait.err")
try {
Write-Host " pid $($proc.Id); warming up for ${WarmupSeconds}s..."
Start-Sleep -Seconds $WarmupSeconds
if ($proc.HasExited) {
throw "Server exited during warmup (code $($proc.ExitCode)). See Logs\measure-$idleWait.err"
}
$proc.Refresh()
$cpuBefore = $proc.TotalProcessorTime
$wallBefore = Get-Date
Write-Host " sampling for ${SampleSeconds}s..."
Start-Sleep -Seconds $SampleSeconds
$proc.Refresh()
$cpuAfter = $proc.TotalProcessorTime
$wallAfter = Get-Date
$cpuMs = ($cpuAfter - $cpuBefore).TotalMilliseconds
$wallMs = ($wallAfter - $wallBefore).TotalMilliseconds
$pct = $cpuMs / $wallMs * 100
Write-Host (" CPU: {0:F2}% of one core ({1:F0}ms CPU over {2:F0}ms wall)" -f $pct, $cpuMs, $wallMs) -ForegroundColor Yellow
[pscustomobject]@{ Label = $label; IdleWait = $idleWait; CpuPercent = $pct }
}
finally {
if (-not $proc.HasExited) {
$proc.Kill()
$proc.WaitForExit(10000) | Out-Null
}
}
}
$legacy = Measure-Loop 0 'Never sleep (max responsiveness)'
$sleeping = Measure-Loop 2 'Idle sleeping'
Write-Host ""
Write-Host "=== Result ===" -ForegroundColor Green
Write-Host (" never sleep : {0,6:F2}% of one core" -f $legacy.CpuPercent)
Write-Host (" idle sleep : {0,6:F2}% of one core" -f $sleeping.CpuPercent)
if ($sleeping.CpuPercent -gt 0) {
Write-Host (" reduction : {0,6:F1}x" -f ($legacy.CpuPercent / $sleeping.CpuPercent))
}
# Leave the config on the new behaviour.
Set-IdleWait 2