Files
truf-server/monitor_runtime_lag.ps1
2026-09-30 20:30:56 +03:00

263 lines
14 KiB
PowerShell

param(
[string]$OutputPath = 'D:\truf\runtime\freeze-diagnostics\runtime-lag.csv',
[int]$IntervalSeconds = 30,
[switch]$Once
)
$ErrorActionPreference = 'Stop'
throw 'Docker development copy: runtime control is disabled until container isolation is ready. See DOCKER_MIGRATION.md.'
$created = $false
$mutex = New-Object System.Threading.Mutex($true, 'Local\TrufRuntimeLagMonitor', [ref]$created)
if (-not $created) {
exit 0
}
try {
$counterTable = (Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\019').Counter
$counterNames = @{}
for ($index = 0; $index -lt $counterTable.Count; $index += 2) {
$counterNames[[int]$counterTable[$index]] = $counterTable[$index + 1]
}
$counterPaths = @(
"\$($counterNames[238])(_Total)\$($counterNames[6])",
"\$($counterNames[4])\$($counterNames[822])",
"\$($counterNames[234])(*)\$($counterNames[198])",
"\$($counterNames[234])(*)\$($counterNames[206])",
"\$($counterNames[234])(*)\$($counterNames[220])",
"\$($counterNames[234])(*)\$($counterNames[222])"
)
$gpuCounterPath = '\GPU Engine(*)\Utilization Percentage'
try {
Get-Counter -Counter $gpuCounterPath -MaxSamples 1 -ErrorAction Stop | Out-Null
$counterPaths += $gpuCounterPath
} catch {
$gpuCounterPath = $null
}
$outputDirectory = Split-Path -Parent $OutputPath
if (-not (Test-Path -LiteralPath $outputDirectory)) {
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
}
if (-not (Test-Path -LiteralPath $OutputPath)) {
[IO.File]::AppendAllText(
$OutputPath,
"timestamp,cpu_percent,free_ram_mb,commit_used_mb,pagefile_used_mb,pages_input_sec,c_queue,c_latency_ms,d_queue,d_latency_ms,s_queue,s_latency_ms,h_queue,h_latency_ms,h_read_bytes_sec,h_write_bytes_sec,supervisor_pid,supervisor_private_mb,supervisor_handles,supervisor_threads,python_private_mb,python_handles,python_threads,largest_pid,largest_name,largest_private_mb,status_age_sec,scan_active,scan_limit,bundle_items,bundle_bytes,projection_items,projection_bytes,keycheck_items,keycheck_bytes,quarantine_items,quarantine_bytes,top_cpu_pid,top_cpu_name,top_cpu_percent,top_handles_pid,top_handles_name,top_handles_count,dpc_percent,interrupt_percent,interrupts_sec,gpu_engine_sum_percent,gpu_top_pid,gpu_top_name,gpu_top_engine_sum_percent`r`n",
[Text.UTF8Encoding]::new($false)
)
}
# PhysicalDisk instances can omit drive letters, so resolve H: to its disk number once.
$hDiskInstancePattern = '*h:*'
try {
foreach ($association in @(Get-CimInstance Win32_LogicalDiskToPartition -ErrorAction Stop)) {
if ($association.Dependent.ToString().Contains('DeviceID = "H:"')) {
$diskMatch = [regex]::Match($association.Antecedent.ToString(), 'Disk #(\d+)')
if ($diskMatch.Success) {
$hDiskInstancePattern = "$($diskMatch.Groups[1].Value)*"
break
}
}
}
} catch {
$hDiskInstancePattern = '*h:*'
}
$previousCpuByPid = @{}
$previousProcessSampleAt = $null
$logicalProcessorCount = [math]::Max(1, [Environment]::ProcessorCount)
while ($true) {
try {
$sampleSets = @(Get-Counter -Counter $counterPaths -SampleInterval 1 -MaxSamples 2)
$samples = $sampleSets[-1].CounterSamples
$cpu = @($samples | Where-Object {
$_.InstanceName -eq '_total' -and $_.Path.EndsWith("\$($counterNames[6])")
})[0].CookedValue
$pagesInput = @($samples | Where-Object {
-not $_.InstanceName -and $_.Path.EndsWith("\$($counterNames[822])")
})[0].CookedValue
$cQueue = @($samples | Where-Object {
$_.InstanceName -like '*c:*' -and $_.Path.EndsWith("\$($counterNames[198])")
})[0].CookedValue
$cLatency = @($samples | Where-Object {
$_.InstanceName -like '*c:*' -and $_.Path.EndsWith("\$($counterNames[206])")
})[0].CookedValue * 1000
$dQueue = @($samples | Where-Object {
$_.InstanceName -like '*d:*' -and $_.Path.EndsWith("\$($counterNames[198])")
})[0].CookedValue
$dLatency = @($samples | Where-Object {
$_.InstanceName -like '*d:*' -and $_.Path.EndsWith("\$($counterNames[206])")
})[0].CookedValue * 1000
$sQueue = @($samples | Where-Object {
$_.InstanceName -like '*s:*' -and $_.Path.EndsWith("\$($counterNames[198])")
})[0].CookedValue
$sLatency = @($samples | Where-Object {
$_.InstanceName -like '*s:*' -and $_.Path.EndsWith("\$($counterNames[206])")
})[0].CookedValue * 1000
$hDiskSamples = @($samples | Where-Object { $_.InstanceName -like $hDiskInstancePattern })
$hQueueSample = @($hDiskSamples | Where-Object {
$_.CounterType.ToString() -eq 'NumberOfItems32'
})
$hLatencySample = @($hDiskSamples | Where-Object {
$_.CounterType.ToString() -eq 'AverageTimer32'
})
$hThroughputSamples = @($hDiskSamples | Where-Object {
$_.CounterType.ToString() -eq 'RateOfCountsPerSecond64'
})
$hReadSample = @($hThroughputSamples | Select-Object -First 1)
$hWriteSample = @($hThroughputSamples | Select-Object -Skip 1 -First 1)
$hQueue = if ($hQueueSample.Count) { $hQueueSample[0].CookedValue } else { -1 }
$hLatency = if ($hLatencySample.Count) { $hLatencySample[0].CookedValue * 1000 } else { -1 }
$hReadBytes = if ($hReadSample.Count) { $hReadSample[0].CookedValue } else { -1 }
$hWriteBytes = if ($hWriteSample.Count) { $hWriteSample[0].CookedValue } else { -1 }
$os = Get-CimInstance Win32_OperatingSystem
$pagefile = Get-CimInstance Win32_PageFileUsage | Where-Object { $_.Name -like 'D:*' }
$python = @(Get-Process -Name python -ErrorAction SilentlyContinue)
$allProcesses = @(Get-Process -ErrorAction SilentlyContinue)
$largest = $allProcesses | Sort-Object PrivateMemorySize64 -Descending | Select-Object -First 1
$mostHandles = $allProcesses | Sort-Object HandleCount -Descending | Select-Object -First 1
$processSampleAt = Get-Date
$elapsedMilliseconds = if ($previousProcessSampleAt) {
($processSampleAt - $previousProcessSampleAt).TotalMilliseconds
} else { 0 }
$currentCpuByPid = @{}
$topCpu = $null
$topCpuPercent = 0.0
foreach ($process in $allProcesses) {
try {
$processId = [int]$process.Id
$totalProcessorMilliseconds = $process.TotalProcessorTime.TotalMilliseconds
$currentCpuByPid[$processId] = $totalProcessorMilliseconds
if ($elapsedMilliseconds -gt 0 -and $previousCpuByPid.ContainsKey($processId)) {
$deltaMilliseconds = [math]::Max(
0,
$totalProcessorMilliseconds - [double]$previousCpuByPid[$processId]
)
$processCpuPercent = 100 * $deltaMilliseconds / ($elapsedMilliseconds * $logicalProcessorCount)
if ($processCpuPercent -gt $topCpuPercent) {
$topCpu = $process
$topCpuPercent = $processCpuPercent
}
}
} catch {
continue
}
}
$previousCpuByPid = $currentCpuByPid
$previousProcessSampleAt = $processSampleAt
$processorPerf = Get-CimInstance `
Win32_PerfFormattedData_PerfOS_Processor `
-Filter "Name='_Total'" `
-ErrorAction SilentlyContinue
$gpuSamples = if ($gpuCounterPath) {
@($samples | Where-Object { $_.Path -like '*\gpu engine(*)\utilization percentage' })
} else { @() }
$gpuEngineSum = [double](($gpuSamples | Measure-Object CookedValue -Sum).Sum)
$gpuByPid = @{}
foreach ($sample in $gpuSamples) {
if ($sample.InstanceName -match '^pid_(\d+)_') {
$gpuProcessId = [int]$matches[1]
if (-not $gpuByPid.ContainsKey($gpuProcessId)) {
$gpuByPid[$gpuProcessId] = 0.0
}
$gpuByPid[$gpuProcessId] += [double]$sample.CookedValue
}
}
$gpuTop = $gpuByPid.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1
$gpuTopProcess = if ($gpuTop) {
$allProcesses | Where-Object { $_.Id -eq [int]$gpuTop.Key } | Select-Object -First 1
} else { $null }
$supervisor = $null
$instancePath = 'D:\truf\runtime\control\supervisor.instance.json'
if (Test-Path -LiteralPath $instancePath) {
$metadata = [IO.File]::ReadAllText($instancePath) | ConvertFrom-Json
$supervisor = Get-Process -Id ([int]$metadata.pid) -ErrorAction SilentlyContinue
}
$statusPath = 'D:\truf\runtime\logs\supervisor.status.txt'
$statusAge = if (Test-Path -LiteralPath $statusPath) {
[math]::Max(0, ((Get-Date) - (Get-Item -LiteralPath $statusPath).LastWriteTime).TotalSeconds)
} else { -1 }
$statusText = if (Test-Path -LiteralPath $statusPath) { [IO.File]::ReadAllText($statusPath) } else { '' }
$scanMatch = [regex]::Match($statusText, 'Scan workers: active=(\d+)/(\d+)')
$pipelineMatch = [regex]::Match(
$statusText,
'bundles=(\d+)/(\d+)B\s+projection=(\d+)/(\d+)B\s+candidates=(\d+)/(\d+)B\s+quarantine=(\d+)/(\d+)B'
)
$values = @(
(Get-Date).ToString('o'),
[math]::Round($cpu, 2),
[math]::Round($os.FreePhysicalMemory / 1KB, 1),
[math]::Round(($os.TotalVirtualMemorySize - $os.FreeVirtualMemory) / 1KB, 1),
[math]::Round((($pagefile | Measure-Object CurrentUsage -Sum).Sum), 1),
[math]::Round($pagesInput, 2),
[math]::Round($cQueue, 2),
[math]::Round($cLatency, 2),
[math]::Round($dQueue, 2),
[math]::Round($dLatency, 2),
[math]::Round($sQueue, 2),
[math]::Round($sLatency, 2),
[math]::Round($hQueue, 2),
[math]::Round($hLatency, 2),
[math]::Round($hReadBytes, 2),
[math]::Round($hWriteBytes, 2),
$(if ($supervisor) { $supervisor.Id } else { 0 }),
$(if ($supervisor) { [math]::Round($supervisor.PrivateMemorySize64 / 1MB, 1) } else { 0 }),
$(if ($supervisor) { $supervisor.HandleCount } else { 0 }),
$(if ($supervisor) { $supervisor.Threads.Count } else { 0 }),
[math]::Round((($python | Measure-Object PrivateMemorySize64 -Sum).Sum / 1MB), 1),
[int](($python | Measure-Object HandleCount -Sum).Sum),
[int](($python | ForEach-Object { $_.Threads.Count } | Measure-Object -Sum).Sum),
$(if ($largest) { $largest.Id } else { 0 }),
$(if ($largest) { ($largest.ProcessName -replace ',', '_') } else { '' }),
$(if ($largest) { [math]::Round($largest.PrivateMemorySize64 / 1MB, 1) } else { 0 }),
[math]::Round($statusAge, 1),
$(if ($scanMatch.Success) { [int64]$scanMatch.Groups[1].Value } else { 0 }),
$(if ($scanMatch.Success) { [int64]$scanMatch.Groups[2].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[1].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[2].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[3].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[4].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[5].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[6].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[7].Value } else { 0 }),
$(if ($pipelineMatch.Success) { [int64]$pipelineMatch.Groups[8].Value } else { 0 }),
$(if ($topCpu) { $topCpu.Id } else { 0 }),
$(if ($topCpu) { ($topCpu.ProcessName -replace ',', '_') } else { '' }),
[math]::Round($topCpuPercent, 2),
$(if ($mostHandles) { $mostHandles.Id } else { 0 }),
$(if ($mostHandles) { ($mostHandles.ProcessName -replace ',', '_') } else { '' }),
$(if ($mostHandles) { $mostHandles.HandleCount } else { 0 }),
$(if ($processorPerf) { [math]::Round($processorPerf.PercentDPCTime, 2) } else { 0 }),
$(if ($processorPerf) { [math]::Round($processorPerf.PercentInterruptTime, 2) } else { 0 }),
$(if ($processorPerf) { [math]::Round($processorPerf.InterruptsPersec, 2) } else { 0 }),
[math]::Round($gpuEngineSum, 2),
$(if ($gpuTop) { [int]$gpuTop.Key } else { 0 }),
$(if ($gpuTopProcess) { ($gpuTopProcess.ProcessName -replace ',', '_') } else { '' }),
$(if ($gpuTop) { [math]::Round([double]$gpuTop.Value, 2) } else { 0 })
)
$serializedValues = foreach ($value in $values) {
if ($value -is [IFormattable]) {
$value.ToString($null, [Globalization.CultureInfo]::InvariantCulture)
} else {
[string]$value
}
}
[IO.File]::AppendAllText($OutputPath, (($serializedValues -join ',') + "`r`n"), [Text.UTF8Encoding]::new($false))
} catch {
[IO.File]::AppendAllText(
"$OutputPath.errors.log",
"$(Get-Date -Format o) $($_.Exception.Message)`r`n",
[Text.UTF8Encoding]::new($false)
)
}
if ($Once) {
break
}
Start-Sleep -Seconds ([math]::Max(5, $IntervalSeconds))
}
} finally {
$mutex.ReleaseMutex()
$mutex.Dispose()
}