param(
[Parameter(Mandatory = $true)][string]$TracePath,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$trace = Get-Item -LiteralPath $TracePath
if ($trace.PSIsContainer -or $trace.Length -gt 8MB) { throw 'Expected one bounded QEMU console_refresh trace file, at most 8 MiB.' }
$records = @()
$lineNumber = 0
foreach ($line in Get-Content -LiteralPath $trace.FullName -Encoding UTF8) {
$lineNumber++
if ($line -notmatch '\bconsole_refresh\b.*?\binterval ([0-9]+) ms\b') { continue }
$interval = [long]$Matches[1]
if ($interval -lt 1 -or $interval -gt 60000) { throw 'The recorded GUI polling interval is outside the supported 1..60000 ms range.' }
if ($records.Count -ge 4096) { throw 'More than 4096 interval-change records; inspect the original trace.' }
$records += [ordered]@{Line=$lineNumber;ConfiguredIntervalMilliseconds=$interval;OriginalRecord=$line}
}
$intervals = @($records | ForEach-Object { $_.ConfiguredIntervalMilliseconds } | Sort-Object -Unique)
$summary = [ordered]@{
Kind='QemuConsoleRefreshIntervalAnnouncements'
TracePath=$trace.FullName
TraceBytesAtRead=[long]$trace.Length
ReadUtc=[DateTime]::UtcNow.ToString('o')
HasConsoleRefreshRecords=($records.Count -gt 0)
RecordCount=$records.Count
ObservedConfiguredIntervalsMilliseconds=$intervals
LastConfiguredIntervalMilliseconds=$(if ($records.Count) { $records[-1].ConfiguredIntervalMilliseconds } else { $null })
Records=$records
Meaning='Interval-change announcements for the QEMU GUI polling timer. These records are not per-frame timestamps, actual visible FPS, monitor scanout or VSync measurements.'
ActualVisibleFramesPerSecond=$null
PhysicalRefreshHz=$null
VSyncVerified=$false
MissingRecordsAreUnknown=$true
LiveFileMayContinueGrowing=$true
ProcessStarted=$false
ProcessStopped=$false
HostDisplayModeChanged=$false
}
if ($OutputPath) {
# A new summary never overwrites an earlier observation or a launch receipt.
$destination = [IO.Path]::GetFullPath($OutputPath)
$bytes = [Text.Encoding]::UTF8.GetBytes(($summary | ConvertTo-Json -Depth 6))
$output = [IO.File]::Open($destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::Read)
try { $output.Write($bytes, 0, $bytes.Length); $output.Flush() } finally { $output.Dispose() }
}
[pscustomobject]$summary
param(
[Parameter(Mandatory = $true)][string]$TracePath,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$trace = Get-Item -LiteralPath $TracePath
if ($trace.PSIsContainer -or $trace.Length -gt 8MB) { throw 'Expected one bounded QEMU console_refresh trace file, at most 8 MiB.' }
$records = @()
$lineNumber = 0
foreach ($line in Get-Content -LiteralPath $trace.FullName -Encoding UTF8) {
$lineNumber++
if ($line -notmatch '\bconsole_refresh\b.*?\binterval ([0-9]+) ms\b') { continue }
$interval = [long]$Matches[1]
if ($interval -lt 1 -or $interval -gt 60000) { throw 'The recorded GUI polling interval is outside the supported 1..60000 ms range.' }
if ($records.Count -ge 4096) { throw 'More than 4096 interval-change records; inspect the original trace.' }
$records += [ordered]@{Line=$lineNumber;ConfiguredIntervalMilliseconds=$interval;OriginalRecord=$line}
}
$intervals = @($records | ForEach-Object { $_.ConfiguredIntervalMilliseconds } | Sort-Object -Unique)
$summary = [ordered]@{
Kind='QemuConsoleRefreshIntervalAnnouncements'
TracePath=$trace.FullName
TraceBytesAtRead=[long]$trace.Length
ReadUtc=[DateTime]::UtcNow.ToString('o')
HasConsoleRefreshRecords=($records.Count -gt 0)
RecordCount=$records.Count
ObservedConfiguredIntervalsMilliseconds=$intervals
LastConfiguredIntervalMilliseconds=$(if ($records.Count) { $records[-1].ConfiguredIntervalMilliseconds } else { $null })
Records=$records
Meaning='Interval-change announcements for the QEMU GUI polling timer. These records are not per-frame timestamps, actual visible FPS, monitor scanout or VSync measurements.'
ActualVisibleFramesPerSecond=$null
PhysicalRefreshHz=$null
VSyncVerified=$false
MissingRecordsAreUnknown=$true
LiveFileMayContinueGrowing=$true
ProcessStarted=$false
ProcessStopped=$false
HostDisplayModeChanged=$false
}
if ($OutputPath) {
# A new summary never overwrites an earlier observation or a launch receipt.
$destination = [IO.Path]::GetFullPath($OutputPath)
$bytes = [Text.Encoding]::UTF8.GetBytes(($summary | ConvertTo-Json -Depth 6))
$output = [IO.File]::Open($destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::Read)
try { $output.Write($bytes, 0, $bytes.Length); $output.Flush() } finally { $output.Dispose() }
}
[pscustomobject]$summary