[CmdletBinding(DefaultParameterSetName = "Run")]
|
param(
|
[Parameter(Mandatory = $true, ParameterSetName = "Run")]
|
[string]$PythonPath,
|
|
[Parameter(Mandatory = $true, ParameterSetName = "Run")]
|
[string]$ScriptPath,
|
|
[Parameter(Mandatory = $true, ParameterSetName = "Run")]
|
[string]$VideoPath,
|
|
[Parameter(Mandatory = $true, ParameterSetName = "Run")]
|
[string]$OutputPath,
|
|
[Parameter(Mandatory = $true, ParameterSetName = "Run")]
|
[Parameter(Mandatory = $true, ParameterSetName = "Analyze")]
|
[string]$EvidenceDirectory,
|
|
[Parameter(Mandatory = $true, ParameterSetName = "Analyze")]
|
[string]$AnalyzeSamplesPath
|
)
|
|
Set-StrictMode -Version Latest
|
$ErrorActionPreference = "Stop"
|
$SampleIntervalMilliseconds = 500
|
$MaximumIntervalSeconds = 1.5
|
$RollingWindowSeconds = 5.0
|
$MinimumWindowMembers = 10
|
$CpuThresholdPercent = 85.0
|
$MaximumHighCpuSecondsExclusive = 10.0
|
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
function Resolve-Leaf([string]$Path, [string]$Label) {
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
throw "$Label does not exist: $Path"
|
}
|
return (Resolve-Path -LiteralPath $Path).Path
|
}
|
|
function Resolve-FuturePath([string]$Path) {
|
return [System.IO.Path]::GetFullPath($Path)
|
}
|
|
function Write-JsonFile([string]$Path, $Value) {
|
$Json = $Value | ConvertTo-Json -Depth 30
|
[System.IO.File]::WriteAllText($Path, $Json + [Environment]::NewLine, $Utf8NoBom)
|
}
|
|
function Convert-ToBoolean($Value) {
|
if ($Value -is [bool]) {
|
return $Value
|
}
|
return [string]$Value -match '^(?i:true|1|yes)$'
|
}
|
|
function Get-RowText($Row, [string]$Name) {
|
$Property = $Row.PSObject.Properties[$Name]
|
if ($null -eq $Property -or $null -eq $Property.Value) {
|
return ""
|
}
|
return [string]$Property.Value
|
}
|
|
function Convert-ToMetric([string]$Value) {
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
return [double]::NaN
|
}
|
return [double]::Parse($Value, [Globalization.CultureInfo]::InvariantCulture)
|
}
|
|
function Test-ScanFfmpegCommand([string]$CommandLine) {
|
return (
|
$CommandLine -match '(?i)(?:^|\s)-map\s+0:v:0(?:\s|$)' -and
|
$CommandLine -match '(?i)fps=fps=1:start_time=0' -and
|
$CommandLine -match '(?i)(?:^|\s)-f\s+rawvideo(?:\s|$)' -and
|
$CommandLine -match '(?i)(?:^|\s)-pix_fmt\s+rgb24(?:\s|$)'
|
)
|
}
|
|
function Get-DecoderFromCommand([string]$CommandLine) {
|
if ($CommandLine -match '(?i)(?:^|\s)-c:v\s+(h264_cuvid|hevc_cuvid)(?:\s|$)') {
|
return [string]$Matches[1].ToLowerInvariant()
|
}
|
return ""
|
}
|
|
function Test-NvdecScanCommand([string]$CommandLine, [string]$Decoder) {
|
if ([string]::IsNullOrWhiteSpace($CommandLine) -or
|
$Decoder -notmatch '^(h264_cuvid|hevc_cuvid)$') {
|
return $false
|
}
|
$InputMatch = [regex]::Match($CommandLine, '(?i)(?:^|\s)-i(?:\s|$)')
|
if (-not $InputMatch.Success) {
|
return $false
|
}
|
$InputPrefix = $CommandLine.Substring(0, $InputMatch.Index + $InputMatch.Length)
|
return (
|
$InputPrefix -match '(?i)(?:^|\s)-hwaccel\s+cuda(?:\s|$)' -and
|
$InputPrefix -match '(?i)(?:^|\s)-hwaccel_output_format\s+cuda(?:\s|$)' -and
|
$InputPrefix -match "(?i)(?:^|\s)-c:v\s+$([regex]::Escape($Decoder))(?:\s|$)" -and
|
$InputPrefix -match '(?i)(?:^|\s)-threads\s+4(?:\s|$)' -and
|
$InputPrefix -match '(?i)(?:^|\s)-filter_threads\s+2(?:\s|$)' -and
|
$InputPrefix -match '(?i)(?:^|\s)-filter_complex_threads\s+2(?:\s|$)' -and
|
$CommandLine -match '(?i)hwdownload,format=nv12' -and
|
(Test-ScanFfmpegCommand $CommandLine)
|
)
|
}
|
|
function Quote-ProcessArgument([string]$Value) {
|
if ($Value.Length -eq 0) {
|
return '""'
|
}
|
if ($Value -notmatch '[\s"]') {
|
return $Value
|
}
|
return '"' + ($Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"'
|
}
|
|
function Get-OverlapSeconds([DateTime]$LeftStart, [DateTime]$LeftEnd, [DateTime]$RightStart, [DateTime]$RightEnd) {
|
$Start = if ($LeftStart -gt $RightStart) { $LeftStart } else { $RightStart }
|
$End = if ($LeftEnd -lt $RightEnd) { $LeftEnd } else { $RightEnd }
|
if ($End -le $Start) {
|
return 0.0
|
}
|
return ($End - $Start).TotalSeconds
|
}
|
|
function Complete-HighCpuSegment($Segments, $Current) {
|
if ($null -eq $Current) {
|
return $null
|
}
|
[void]$Segments.Add([pscustomobject][ordered]@{
|
start_timestamp_utc = $Current.start.ToString("o")
|
end_timestamp_utc = $Current.end.ToString("o")
|
duration_seconds = [Math]::Round(($Current.end - $Current.start).TotalSeconds, 6)
|
member_rows = ($Current.members -join ";")
|
termination_reason = [string]$Current.reason
|
})
|
return $null
|
}
|
|
function Analyze-ResourceSamples([string]$SamplesPath, [string]$OutputDirectory) {
|
$Rows = @(Import-Csv -LiteralPath $SamplesPath)
|
$Problems = New-Object System.Collections.Generic.List[string]
|
$RequiredColumns = @(
|
"timestamp_utc", "previous_valid_timestamp_utc", "interval_seconds",
|
"counter_warmup", "cpu_valid", "host_cpu_percent",
|
"scan_phase_active", "scan_ffmpeg_present", "scan_ffmpeg_exit_observed",
|
"scan_ffmpeg_pid", "scan_ffmpeg_command_line", "decoder",
|
"logical_processor_count", "task_cpu_percent", "task_working_set_bytes",
|
"task_pids", "root_priority", "ffmpeg_priorities", "ffmpeg_command_lines",
|
"gpu_query_status", "gpu_query_raw_rows", "gpu_utilization_percent",
|
"gpu_decoder_utilization_percent", "gpu_memory_used_mib",
|
"nvidia_smi_compute_query_status", "nvidia_smi_compute_raw_rows",
|
"nvidia_smi_compute_task_rows",
|
"gpu_process_memory_query_status", "gpu_process_memory_instances",
|
"task_gpu_dedicated_memory_mib"
|
)
|
$Headers = if ($Rows.Count -gt 0) { @($Rows[0].PSObject.Properties.Name) } else { @() }
|
if ($Rows.Count -eq 0) {
|
[void]$Problems.Add("samples_empty")
|
}
|
foreach ($Column in $RequiredColumns) {
|
if ($Headers -notcontains $Column) {
|
[void]$Problems.Add("required_column_missing_$Column")
|
}
|
}
|
|
$Parsed = New-Object System.Collections.Generic.List[object]
|
$PreviousParsedTimestamp = $null
|
for ($Index = 0; $Index -lt $Rows.Count; $Index++) {
|
$Row = $Rows[$Index]
|
$RowNumber = $Index + 2
|
try {
|
$Timestamp = [DateTime]::Parse(
|
(Get-RowText $Row "timestamp_utc"),
|
[Globalization.CultureInfo]::InvariantCulture,
|
[Globalization.DateTimeStyles]::RoundtripKind
|
).ToUniversalTime()
|
} catch {
|
[void]$Problems.Add("timestamp_invalid_row_$RowNumber")
|
continue
|
}
|
$Previous = $null
|
$PreviousText = Get-RowText $Row "previous_valid_timestamp_utc"
|
if (-not [string]::IsNullOrWhiteSpace($PreviousText)) {
|
try {
|
$Previous = [DateTime]::Parse(
|
$PreviousText,
|
[Globalization.CultureInfo]::InvariantCulture,
|
[Globalization.DateTimeStyles]::RoundtripKind
|
).ToUniversalTime()
|
} catch {
|
[void]$Problems.Add("previous_timestamp_invalid_row_$RowNumber")
|
}
|
}
|
if ($Index -eq 0) {
|
if ($null -ne $Previous) {
|
[void]$Problems.Add("first_row_previous_must_be_empty")
|
}
|
$ActualInterval = 0.0
|
} else {
|
if ($null -eq $Previous) {
|
[void]$Problems.Add("previous_timestamp_missing_row_$RowNumber")
|
} elseif ([Math]::Abs(($Previous - $PreviousParsedTimestamp).TotalMilliseconds) -gt 1.0) {
|
[void]$Problems.Add("previous_timestamp_mismatch_row_$RowNumber")
|
}
|
$ActualInterval = ($Timestamp - $PreviousParsedTimestamp).TotalSeconds
|
if ($ActualInterval -le 0) {
|
[void]$Problems.Add("timestamp_not_strictly_increasing_row_$RowNumber")
|
}
|
}
|
$ReportedIntervalText = Get-RowText $Row "interval_seconds"
|
try {
|
$ReportedInterval = Convert-ToMetric $ReportedIntervalText
|
if ([double]::IsNaN($ReportedInterval) -or [Math]::Abs($ReportedInterval - $ActualInterval) -gt 0.001) {
|
[void]$Problems.Add("interval_mismatch_row_$RowNumber")
|
}
|
} catch {
|
[void]$Problems.Add("interval_invalid_row_$RowNumber")
|
}
|
|
$ScanActive = Convert-ToBoolean (Get-RowText $Row "scan_phase_active")
|
$Warmup = Convert-ToBoolean (Get-RowText $Row "counter_warmup")
|
if ($ScanActive) {
|
foreach ($Field in $RequiredColumns) {
|
if ($Field -eq "previous_valid_timestamp_utc" -and $Index -eq 0) { continue }
|
if (($Field -eq "host_cpu_percent" -or $Field -eq "task_cpu_percent") -and $Warmup) { continue }
|
if ([string]::IsNullOrWhiteSpace((Get-RowText $Row $Field))) {
|
[void]$Problems.Add("required_value_missing_${Field}_row_$RowNumber")
|
}
|
}
|
foreach ($StatusField in @(
|
"gpu_query_status", "nvidia_smi_compute_query_status", "gpu_process_memory_query_status"
|
)) {
|
if ((Get-RowText $Row $StatusField) -ne "ok") {
|
[void]$Problems.Add("query_not_ok_${StatusField}_row_$RowNumber")
|
}
|
}
|
}
|
$PreviousForInterval = if ($Index -eq 0) { $null } else { $PreviousParsedTimestamp }
|
[void]$Parsed.Add([pscustomobject]@{
|
row_number = $RowNumber
|
timestamp = $Timestamp
|
previous = $PreviousForInterval
|
interval = $ActualInterval
|
warmup = $Warmup
|
cpu_valid = Convert-ToBoolean (Get-RowText $Row "cpu_valid")
|
scan_active = $ScanActive
|
scan_present = Convert-ToBoolean (Get-RowText $Row "scan_ffmpeg_present")
|
scan_exit_observed = Convert-ToBoolean (Get-RowText $Row "scan_ffmpeg_exit_observed")
|
scan_pid = Get-RowText $Row "scan_ffmpeg_pid"
|
scan_command = Get-RowText $Row "scan_ffmpeg_command_line"
|
decoder = Get-RowText $Row "decoder"
|
logical_processors = Convert-ToMetric (Get-RowText $Row "logical_processor_count")
|
host_cpu = Convert-ToMetric (Get-RowText $Row "host_cpu_percent")
|
task_cpu = Convert-ToMetric (Get-RowText $Row "task_cpu_percent")
|
task_ram = Convert-ToMetric (Get-RowText $Row "task_working_set_bytes")
|
gpu_util = Convert-ToMetric (Get-RowText $Row "gpu_utilization_percent")
|
decoder_util = Convert-ToMetric (Get-RowText $Row "gpu_decoder_utilization_percent")
|
gpu_memory = Convert-ToMetric (Get-RowText $Row "gpu_memory_used_mib")
|
task_gpu_memory = Convert-ToMetric (Get-RowText $Row "task_gpu_dedicated_memory_mib")
|
})
|
$PreviousParsedTimestamp = $Timestamp
|
}
|
|
$Active = @($Parsed | Where-Object { $_.scan_active })
|
if ($Active.Count -eq 0) {
|
[void]$Problems.Add("scan_ffmpeg_not_observed")
|
} else {
|
$FirstActiveIndex = [array]::IndexOf([object[]]$Parsed.ToArray(), $Active[0])
|
$LastActiveIndex = [array]::IndexOf([object[]]$Parsed.ToArray(), $Active[-1])
|
for ($Index = $FirstActiveIndex; $Index -le $LastActiveIndex; $Index++) {
|
if (-not $Parsed[$Index].scan_active) {
|
[void]$Problems.Add("scan_phase_not_contiguous_row_$($Parsed[$Index].row_number)")
|
}
|
}
|
if (-not $Active[0].scan_present) {
|
[void]$Problems.Add("scan_phase_first_row_not_present")
|
}
|
if ($Active[-1].scan_present -or -not $Active[-1].scan_exit_observed) {
|
[void]$Problems.Add("scan_phase_last_row_not_exit_observation")
|
}
|
$ScanPids = @($Active | ForEach-Object { $_.scan_pid } | Where-Object { $_ } | Select-Object -Unique)
|
if ($ScanPids.Count -ne 1) {
|
[void]$Problems.Add("scan_pid_not_unique")
|
}
|
$ScanCommands = @($Active | ForEach-Object { $_.scan_command } | Where-Object { $_ } | Select-Object -Unique)
|
if ($ScanCommands.Count -ne 1) {
|
[void]$Problems.Add("scan_command_not_unique")
|
}
|
$Decoders = @($Active | ForEach-Object { $_.decoder } | Where-Object { $_ } | Select-Object -Unique)
|
if ($Decoders.Count -ne 1) {
|
[void]$Problems.Add("decoder_not_unique")
|
}
|
$LogicalProcessorCounts = @($Active | ForEach-Object { $_.logical_processors } | Select-Object -Unique)
|
if ($LogicalProcessorCounts.Count -ne 1 -or [double]$LogicalProcessorCounts[0] -le 0) {
|
[void]$Problems.Add("logical_processor_count_invalid")
|
}
|
if ($ScanCommands.Count -eq 1 -and $Decoders.Count -eq 1 -and
|
-not (Test-NvdecScanCommand $ScanCommands[0] $Decoders[0])) {
|
[void]$Problems.Add("nvdec_scan_argv_invalid")
|
}
|
}
|
$ValidActive = @($Active | Where-Object {
|
-not $_.warmup -and $_.cpu_valid -and
|
$null -ne $_.previous -and
|
-not [double]::IsNaN($_.host_cpu)
|
})
|
if ($ValidActive.Count -eq 0) {
|
[void]$Problems.Add("no_valid_scan_cpu_samples")
|
}
|
foreach ($Row in $ValidActive) {
|
if ($Row.interval -le 0 -or $Row.interval -gt $MaximumIntervalSeconds) {
|
[void]$Problems.Add("invalid_interval_row_$($Row.row_number)=$($Row.interval)")
|
}
|
}
|
|
$ScanStart = $null
|
$ScanEnd = $null
|
if ($ValidActive.Count -gt 0) {
|
$ScanStart = $ValidActive[0].previous
|
$ScanEnd = $ValidActive[-1].timestamp
|
}
|
$Windows = New-Object System.Collections.Generic.List[object]
|
if ($null -ne $ScanStart -and $null -ne $ScanEnd) {
|
foreach ($EndRow in $ValidActive) {
|
$WindowEnd = $EndRow.timestamp
|
$WindowStart = $WindowEnd.AddSeconds(-$RollingWindowSeconds)
|
if ($WindowStart -lt $ScanStart) {
|
continue
|
}
|
$Members = New-Object System.Collections.Generic.List[object]
|
$Coverage = 0.0
|
$WeightedCpu = 0.0
|
$MaximumInterval = 0.0
|
foreach ($Row in $ValidActive) {
|
$Overlap = Get-OverlapSeconds $Row.previous $Row.timestamp $WindowStart $WindowEnd
|
if ($Overlap -le 0) {
|
continue
|
}
|
[void]$Members.Add($Row)
|
$Coverage += $Overlap
|
$WeightedCpu += $Row.host_cpu * $Overlap
|
$MaximumInterval = [Math]::Max($MaximumInterval, $Row.interval)
|
}
|
$MemberCount = $Members.Count
|
$Dense = (
|
$Coverage -ge ($RollingWindowSeconds - 0.001) -and
|
$MemberCount -ge $MinimumWindowMembers -and
|
$MaximumInterval -le $MaximumIntervalSeconds
|
)
|
$WindowCpu = if ($Coverage -gt 0) { $WeightedCpu / $Coverage } else { [double]::NaN }
|
[void]$Windows.Add([pscustomobject][ordered]@{
|
window_start_utc = $WindowStart.ToString("o")
|
window_end_utc = $WindowEnd.ToString("o")
|
member_rows = (($Members | ForEach-Object { $_.row_number }) -join ";")
|
member_count = $MemberCount
|
coverage_seconds = [Math]::Round($Coverage, 6)
|
maximum_interval_seconds = [Math]::Round($MaximumInterval, 6)
|
weighted_host_cpu_percent = if ([double]::IsNaN($WindowCpu)) { "" } else { [Math]::Round($WindowCpu, 6) }
|
density_valid = $Dense
|
cpu_below_85 = (-not [double]::IsNaN($WindowCpu) -and $WindowCpu -lt $CpuThresholdPercent)
|
})
|
if (-not $Dense) {
|
[void]$Problems.Add("window_density_invalid_$($WindowEnd.ToString('o'))")
|
}
|
}
|
}
|
if ($Windows.Count -eq 0) {
|
[void]$Problems.Add("window_count_zero")
|
}
|
|
$HighSegments = New-Object System.Collections.Generic.List[object]
|
$CurrentSegment = $null
|
foreach ($Row in $ValidActive) {
|
$IsHigh = $Row.host_cpu -ge $CpuThresholdPercent
|
$IntervalContinuous = $Row.interval -gt 0 -and $Row.interval -le $MaximumIntervalSeconds
|
if ($IsHigh -and $IntervalContinuous) {
|
if ($null -eq $CurrentSegment) {
|
$CurrentSegment = [pscustomobject]@{
|
start = $Row.previous
|
end = $Row.timestamp
|
members = New-Object System.Collections.Generic.List[int]
|
reason = "scan_end"
|
}
|
} elseif ([Math]::Abs(($Row.previous - $CurrentSegment.end).TotalMilliseconds) -gt 1.0) {
|
$CurrentSegment.reason = "timestamp_gap"
|
$CurrentSegment = Complete-HighCpuSegment $HighSegments $CurrentSegment
|
$CurrentSegment = [pscustomobject]@{
|
start = $Row.previous
|
end = $Row.timestamp
|
members = New-Object System.Collections.Generic.List[int]
|
reason = "scan_end"
|
}
|
}
|
$CurrentSegment.end = $Row.timestamp
|
[void]$CurrentSegment.members.Add([int]$Row.row_number)
|
} else {
|
if ($null -ne $CurrentSegment) {
|
$CurrentSegment.reason = if ($IsHigh) { "invalid_interval" } else { "cpu_below_threshold" }
|
$CurrentSegment = Complete-HighCpuSegment $HighSegments $CurrentSegment
|
}
|
}
|
}
|
$CurrentSegment = Complete-HighCpuSegment $HighSegments $CurrentSegment
|
|
$MaximumHighSeconds = 0.0
|
$LongestHighSegment = $null
|
if ($HighSegments.Count -gt 0) {
|
$MaximumHighSeconds = [double](($HighSegments | Measure-Object -Property duration_seconds -Maximum).Maximum)
|
$LongestHighSegment = $HighSegments | Sort-Object { [double]$_.duration_seconds } -Descending | Select-Object -First 1
|
}
|
$AllWindowCpuPass = $Windows.Count -gt 0 -and @(
|
$Windows | Where-Object { -not (Convert-ToBoolean $_.cpu_below_85) }
|
).Count -eq 0
|
$SamplingValid = $Problems.Count -eq 0
|
$CpuGatePass = (
|
$SamplingValid -and
|
$AllWindowCpuPass -and
|
$MaximumHighSeconds -lt $MaximumHighCpuSecondsExclusive
|
)
|
$MaximumObservedInterval = 0.0
|
if ($ValidActive.Count -gt 0) {
|
$MaximumObservedInterval = [double](($ValidActive | Measure-Object -Property interval -Maximum).Maximum)
|
}
|
$WorstWindow = $null
|
if ($Windows.Count -gt 0) {
|
$WorstWindow = $Windows | Sort-Object { [double]$_.weighted_host_cpu_percent } -Descending | Select-Object -First 1
|
}
|
function Get-MetricPeak([object[]]$MetricRows, [string]$Property) {
|
$Values = @($MetricRows | ForEach-Object { $_.$Property } | Where-Object { -not [double]::IsNaN([double]$_) })
|
if ($Values.Count -eq 0) { return $null }
|
return [double](($Values | Measure-Object -Maximum).Maximum)
|
}
|
|
$WindowsPath = Join-Path $OutputDirectory "resource_windows.csv"
|
$SegmentsPath = Join-Path $OutputDirectory "high_cpu_segments.csv"
|
$SummaryPath = Join-Path $OutputDirectory "resource_summary.json"
|
$Windows | Export-Csv -LiteralPath $WindowsPath -NoTypeInformation -Encoding UTF8
|
$HighSegments | Export-Csv -LiteralPath $SegmentsPath -NoTypeInformation -Encoding UTF8
|
$Summary = [ordered]@{
|
generated_at_utc = [DateTime]::UtcNow.ToString("o")
|
sampling_valid = $SamplingValid
|
sampling_problems = @($Problems)
|
counter_warmup_rows = @($Parsed | Where-Object { $_.warmup }).Count
|
valid_scan_sample_count = $ValidActive.Count
|
scan_start_utc = if ($null -eq $ScanStart) { $null } else { $ScanStart.ToString("o") }
|
scan_end_utc = if ($null -eq $ScanEnd) { $null } else { $ScanEnd.ToString("o") }
|
scan_ffmpeg_pid = if ($Active.Count -eq 0) { $null } else { $Active[0].scan_pid }
|
scan_ffmpeg_command_line = if ($Active.Count -eq 0) { $null } else { $Active[0].scan_command }
|
decoder = if ($Active.Count -eq 0) { $null } else { $Active[0].decoder }
|
logical_processor_count = if ($Active.Count -eq 0) { $null } else { $Active[0].logical_processors }
|
nvdec_scan_argv_evidence = if ($Active.Count -eq 0) {
|
$false
|
} else {
|
Test-NvdecScanCommand $Active[0].scan_command $Active[0].decoder
|
}
|
configured_interval_milliseconds = $SampleIntervalMilliseconds
|
maximum_observed_interval_seconds = [Math]::Round($MaximumObservedInterval, 6)
|
rolling_window_seconds = $RollingWindowSeconds
|
minimum_window_members = $MinimumWindowMembers
|
window_count = $Windows.Count
|
worst_window = $WorstWindow
|
all_windows_host_cpu_strictly_below_85 = $AllWindowCpuPass
|
high_cpu_threshold_percent = $CpuThresholdPercent
|
maximum_high_cpu_segment_seconds = [Math]::Round($MaximumHighSeconds, 6)
|
longest_high_cpu_segment = $LongestHighSegment
|
maximum_high_cpu_seconds_exclusive = $MaximumHighCpuSecondsExclusive
|
cpu_gate_pass = $CpuGatePass
|
task_cpu_peak_percent = Get-MetricPeak $ValidActive "task_cpu"
|
task_working_set_peak_bytes = Get-MetricPeak $ValidActive "task_ram"
|
gpu_utilization_peak_percent = Get-MetricPeak $ValidActive "gpu_util"
|
gpu_decoder_utilization_peak_percent = Get-MetricPeak $ValidActive "decoder_util"
|
gpu_memory_used_peak_mib = Get-MetricPeak $ValidActive "gpu_memory"
|
task_gpu_dedicated_memory_peak_mib = Get-MetricPeak $ValidActive "task_gpu_memory"
|
}
|
Write-JsonFile $SummaryPath $Summary
|
return [pscustomobject]@{
|
Summary = $Summary
|
SummaryPath = $SummaryPath
|
WindowsPath = $WindowsPath
|
SegmentsPath = $SegmentsPath
|
}
|
}
|
|
if ($PSCmdlet.ParameterSetName -eq "Analyze") {
|
$AnalyzeSamplesPath = Resolve-Leaf $AnalyzeSamplesPath "samples CSV"
|
$EvidenceDirectory = Resolve-FuturePath $EvidenceDirectory
|
if (-not (Test-Path -LiteralPath $EvidenceDirectory -PathType Container)) {
|
[void](New-Item -ItemType Directory -Path $EvidenceDirectory)
|
}
|
$Analysis = Analyze-ResourceSamples $AnalyzeSamplesPath $EvidenceDirectory
|
Write-Output "sampling_valid=$($Analysis.Summary.sampling_valid)"
|
Write-Output "window_count=$($Analysis.Summary.window_count)"
|
Write-Output "cpu_gate_pass=$($Analysis.Summary.cpu_gate_pass)"
|
exit 0
|
}
|
|
$PythonPath = Resolve-Leaf $PythonPath "Python"
|
$ScriptPath = Resolve-Leaf $ScriptPath "media script"
|
$VideoPath = Resolve-Leaf $VideoPath "test video"
|
$OutputPath = Resolve-FuturePath $OutputPath
|
$EvidenceDirectory = Resolve-FuturePath $EvidenceDirectory
|
if (Test-Path -LiteralPath $EvidenceDirectory) {
|
throw "Evidence directory already exists; refusing to overwrite: $EvidenceDirectory"
|
}
|
[void](New-Item -ItemType Directory -Path $EvidenceDirectory)
|
|
$StdoutPath = Join-Path $EvidenceDirectory "stdout.log"
|
$StderrPath = Join-Path $EvidenceDirectory "stderr.log"
|
$SamplesPath = Join-Path $EvidenceDirectory "resource_samples.csv"
|
$CommandPath = Join-Path $EvidenceDirectory "command.json"
|
$SourceBeforePath = Join-Path $EvidenceDirectory "source_before.json"
|
$SourceAfterPath = Join-Path $EvidenceDirectory "source_after.json"
|
[System.IO.File]::WriteAllText($StdoutPath, [string]::Empty, $Utf8NoBom)
|
[System.IO.File]::WriteAllText($StderrPath, [string]::Empty, $Utf8NoBom)
|
|
function Get-SourceSnapshot([string]$Path) {
|
$Item = Get-Item -LiteralPath $Path
|
return [ordered]@{
|
path = $Item.FullName
|
size_bytes = [int64]$Item.Length
|
creation_time_utc = $Item.CreationTimeUtc.ToString("o")
|
last_write_time_utc = $Item.LastWriteTimeUtc.ToString("o")
|
sha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
}
|
}
|
Write-JsonFile $SourceBeforePath (Get-SourceSnapshot $VideoPath)
|
|
if (-not ("NativeProcessTree" -as [type])) {
|
Add-Type -TypeDefinition @"
|
using System;
|
using System.Collections.Generic;
|
using System.Runtime.InteropServices;
|
public static class NativeProcessTree {
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
private struct PROCESSENTRY32 {
|
public uint dwSize; public uint cntUsage; public uint th32ProcessID;
|
public IntPtr th32DefaultHeapID; public uint th32ModuleID; public uint cntThreads;
|
public uint th32ParentProcessID; public int pcPriClassBase; public uint dwFlags;
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExeFile;
|
}
|
public sealed class Entry {
|
public int ProcessId; public int ParentProcessId; public string Name;
|
}
|
[DllImport("kernel32.dll", SetLastError=true)] private static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint pid);
|
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] private static extern bool Process32FirstW(IntPtr snapshot, ref PROCESSENTRY32 entry);
|
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] private static extern bool Process32NextW(IntPtr snapshot, ref PROCESSENTRY32 entry);
|
[DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
|
public static Entry[] Snapshot() {
|
IntPtr handle = CreateToolhelp32Snapshot(2, 0);
|
if (handle == new IntPtr(-1)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
var result = new List<Entry>();
|
try {
|
var item = new PROCESSENTRY32(); item.dwSize = (uint)Marshal.SizeOf(item);
|
if (Process32FirstW(handle, ref item)) do {
|
result.Add(new Entry { ProcessId=(int)item.th32ProcessID, ParentProcessId=(int)item.th32ParentProcessID, Name=item.szExeFile });
|
item.dwSize = (uint)Marshal.SizeOf(item);
|
} while (Process32NextW(handle, ref item));
|
} finally { CloseHandle(handle); }
|
return result.ToArray();
|
}
|
}
|
"@
|
}
|
|
$Arguments = @("-X", "utf8", "-u", "-B", $ScriptPath, $VideoPath, "--output", $OutputPath)
|
$ArgumentString = (($Arguments | ForEach-Object { Quote-ProcessArgument $_ }) -join " ")
|
$StartInfo = New-Object System.Diagnostics.ProcessStartInfo
|
$StartInfo.FileName = $PythonPath
|
$StartInfo.Arguments = $ArgumentString
|
$StartInfo.WorkingDirectory = (Get-Location).Path
|
$StartInfo.UseShellExecute = $false
|
$StartInfo.CreateNoWindow = $true
|
$StartInfo.RedirectStandardOutput = $true
|
$StartInfo.RedirectStandardError = $true
|
$StartInfo.StandardOutputEncoding = [Text.Encoding]::UTF8
|
$StartInfo.StandardErrorEncoding = [Text.Encoding]::UTF8
|
$Process = New-Object System.Diagnostics.Process
|
$Process.StartInfo = $StartInfo
|
$Samples = New-Object System.Collections.Generic.List[object]
|
$SamplingErrors = New-Object System.Collections.Generic.List[string]
|
$HostCpuCounter = New-Object Diagnostics.PerformanceCounter("Processor", "% Processor Time", "_Total", $true)
|
[void]$HostCpuCounter.NextValue()
|
$GpuMemoryCategory = $null
|
try {
|
$GpuMemoryCategory = New-Object Diagnostics.PerformanceCounterCategory("GPU Process Memory")
|
[void]$GpuMemoryCategory.GetInstanceNames()
|
} catch {
|
[void]$SamplingErrors.Add("$([DateTime]::UtcNow.ToString('o')) GPU Process Memory category unavailable: $($_.Exception.Message)")
|
}
|
$CimWarmup = @(Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -Property ProcessId,CommandLine -ErrorAction Stop)
|
if ($CimWarmup.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string]$CimWarmup[0].CommandLine)) {
|
throw "CIM Win32_Process command-line warm-up failed"
|
}
|
$GpuWarmupRows = @(& nvidia-smi --query-gpu=utilization.gpu,utilization.decoder,memory.used --format=csv,noheader,nounits 2>&1)
|
if ($LASTEXITCODE -ne 0 -or $GpuWarmupRows.Count -eq 0) {
|
throw "nvidia-smi GPU/decode warm-up failed"
|
}
|
$ComputeWarmupRows = @(& nvidia-smi --query-compute-apps=pid,used_gpu_memory --format=csv,noheader,nounits 2>&1)
|
if ($LASTEXITCODE -ne 0) {
|
throw "nvidia-smi compute-process warm-up failed"
|
}
|
$TestCimDelayMilliseconds = 0
|
if (-not [string]::IsNullOrWhiteSpace($env:MBX_MEDIA_SAMPLER_TEST_CIM_DELAY_MS)) {
|
$TestCimDelayMilliseconds = [int]$env:MBX_MEDIA_SAMPLER_TEST_CIM_DELAY_MS
|
if ($TestCimDelayMilliseconds -lt 0 -or $TestCimDelayMilliseconds -gt 1000) {
|
throw "MBX_MEDIA_SAMPLER_TEST_CIM_DELAY_MS must be 0..1000"
|
}
|
}
|
$InjectSampleFailureOnce = $env:MBX_MEDIA_SAMPLER_TEST_FAIL_SAMPLE_ONCE -eq "1"
|
$InjectedSampleFailureCount = 0
|
$CommandLineDisappearanceRaceCount = 0
|
$StartInfo.EnvironmentVariables["MBX_MEDIA_SAMPLER_READY"] = "1"
|
[void]$Process.Start()
|
$StdoutTask = $Process.StandardOutput.ReadToEndAsync()
|
$StderrTask = $Process.StandardError.ReadToEndAsync()
|
$RootPid = [int]$Process.Id
|
$StartUtc = [DateTime]::UtcNow
|
$Stopwatch = [Diagnostics.Stopwatch]::StartNew()
|
$PreviousCpuByPid = @{}
|
$PreviousCounterTimestamp = $null
|
$CommandLinesByPid = @{}
|
$FirstCounter = $true
|
$LastSchedule = 0L
|
$ScanPid = $null
|
$ScanCommandLine = ""
|
$ScanDecoder = ""
|
$ScanPidWasPresent = $false
|
$ScanPhaseClosed = $false
|
$ScanFfmpegSeen = $false
|
$MultipleScanPidsSeen = $false
|
$ScanCloseCommitted = $false
|
$LogicalProcessorCount = [Environment]::ProcessorCount
|
|
do {
|
try {
|
$Now = [DateTime]::UtcNow
|
$HostCpu = [double]$HostCpuCounter.NextValue()
|
$Warmup = $FirstCounter
|
$CpuValid = -not $Warmup -and -not [double]::IsNaN($HostCpu) -and -not [double]::IsInfinity($HostCpu)
|
$Interval = if ($null -eq $PreviousCounterTimestamp) { 0.0 } else { ($Now - $PreviousCounterTimestamp).TotalSeconds }
|
$PreviousText = if ($null -eq $PreviousCounterTimestamp) { "" } else { $PreviousCounterTimestamp.ToString("o") }
|
|
$Snapshot = @([NativeProcessTree]::Snapshot())
|
$TaskPids = New-Object 'System.Collections.Generic.HashSet[int]'
|
[void]$TaskPids.Add($RootPid)
|
do {
|
$Added = $false
|
foreach ($Entry in $Snapshot) {
|
if ($TaskPids.Contains([int]$Entry.ParentProcessId) -and -not $TaskPids.Contains([int]$Entry.ProcessId)) {
|
[void]$TaskPids.Add([int]$Entry.ProcessId)
|
$Added = $true
|
}
|
}
|
} while ($Added)
|
|
$WorkingSet = 0L
|
$TaskCpu = 0.0
|
$Priorities = New-Object System.Collections.Generic.List[object]
|
$FfmpegPids = New-Object System.Collections.Generic.List[int]
|
foreach ($PidValue in $TaskPids) {
|
$SnapshotEntry = $Snapshot | Where-Object { [int]$_.ProcessId -eq [int]$PidValue } | Select-Object -First 1
|
$SnapshotWasFfmpeg = $null -ne $SnapshotEntry -and [string]$SnapshotEntry.Name -ieq "ffmpeg.exe"
|
try {
|
$TaskProcess = Get-Process -Id $PidValue -ErrorAction Stop
|
$WorkingSet += [int64]$TaskProcess.WorkingSet64
|
$CpuTotal = [double]$TaskProcess.TotalProcessorTime.TotalSeconds
|
if ($CpuValid -and $Interval -gt 0 -and $PreviousCpuByPid.ContainsKey($PidValue)) {
|
$Delta = [Math]::Max(0.0, $CpuTotal - [double]$PreviousCpuByPid[$PidValue])
|
$TaskCpu += $Delta / ($Interval * [Environment]::ProcessorCount) * 100.0
|
}
|
$PreviousCpuByPid[$PidValue] = $CpuTotal
|
[void]$Priorities.Add([ordered]@{ pid=$PidValue; name=$TaskProcess.ProcessName; priority=[string]$TaskProcess.PriorityClass })
|
if ($TaskProcess.ProcessName -ieq "ffmpeg") {
|
[void]$FfmpegPids.Add([int]$PidValue)
|
if (-not $CommandLinesByPid.ContainsKey($PidValue)) {
|
if ($TestCimDelayMilliseconds -gt 0 -and $null -ne $ScanPid) {
|
Start-Sleep -Milliseconds $TestCimDelayMilliseconds
|
}
|
$CimRows = @(Get-CimInstance Win32_Process -Filter "ProcessId=$PidValue" -Property ProcessId,CommandLine -ErrorAction Stop)
|
if ($CimRows.Count -eq 0) {
|
$StillRunning = Get-Process -Id $PidValue -ErrorAction SilentlyContinue
|
if ($null -eq $StillRunning) {
|
$CommandLineDisappearanceRaceCount += 1
|
continue
|
}
|
throw "CommandLine query returned no row for live FFmpeg PID $PidValue"
|
}
|
$QueriedCommandLine = [string]$CimRows[0].CommandLine
|
if ([string]::IsNullOrWhiteSpace($QueriedCommandLine)) {
|
throw "CommandLine query returned an empty value for live FFmpeg PID $PidValue"
|
}
|
$CommandLinesByPid[$PidValue] = $QueriedCommandLine
|
}
|
$CandidateCommand = [string]$CommandLinesByPid[$PidValue]
|
if (Test-ScanFfmpegCommand $CandidateCommand) {
|
if ($null -eq $ScanPid) {
|
$ScanPid = [int]$PidValue
|
$ScanCommandLine = $CandidateCommand
|
$ScanDecoder = Get-DecoderFromCommand $CandidateCommand
|
} elseif ([int]$ScanPid -ne [int]$PidValue) {
|
$MultipleScanPidsSeen = $true
|
[void]$SamplingErrors.Add("$($Now.ToString('o')) multiple stable-scan FFmpeg PIDs: $ScanPid and $PidValue")
|
}
|
}
|
}
|
} catch {
|
$StillRunningAfterError = Get-Process -Id $PidValue -ErrorAction SilentlyContinue
|
if ($null -eq $StillRunningAfterError) {
|
if ($SnapshotWasFfmpeg) {
|
$CommandLineDisappearanceRaceCount += 1
|
}
|
continue
|
}
|
throw
|
}
|
}
|
|
$ScanPresent = $null -ne $ScanPid -and $TaskPids.Contains([int]$ScanPid) -and $FfmpegPids.Contains([int]$ScanPid)
|
$ScanPhaseActive = $false
|
$ScanExitObserved = $false
|
if (-not $ScanPhaseClosed -and $null -ne $ScanPid) {
|
if ($ScanPresent) {
|
$ScanPhaseActive = $true
|
$ScanPidWasPresent = $true
|
$ScanFfmpegSeen = $true
|
} elseif ($ScanPidWasPresent) {
|
$ScanPhaseActive = $true
|
$ScanExitObserved = $true
|
$ScanPhaseClosed = $true
|
}
|
}
|
|
$GpuQueryStatus = "ok"
|
$GpuUtil = [double]::NaN
|
$DecoderUtil = [double]::NaN
|
$GpuMemory = [double]::NaN
|
$GpuRows = @(& nvidia-smi --query-gpu=utilization.gpu,utilization.decoder,memory.used --format=csv,noheader,nounits 2>&1)
|
$GpuExitCode = $LASTEXITCODE
|
$GpuRawRows = [string](ConvertTo-Json -Compress -InputObject @($GpuRows | ForEach-Object { [string]$_ }))
|
try {
|
if ($GpuExitCode -ne 0 -or $GpuRows.Count -eq 0) { throw "exit=$GpuExitCode rows=$GpuRawRows" }
|
$GpuParts = @($GpuRows[0] -split ',' | ForEach-Object { $_.Trim() })
|
if ($GpuParts.Count -lt 3) { throw "unexpected row: $($GpuRows[0])" }
|
$GpuUtil = Convert-ToMetric $GpuParts[0]
|
$DecoderUtil = Convert-ToMetric $GpuParts[1]
|
$GpuMemory = Convert-ToMetric $GpuParts[2]
|
} catch {
|
$GpuQueryStatus = "error"
|
[void]$SamplingErrors.Add("$($Now.ToString('o')) nvidia-smi GPU/decode query failed: $($_.Exception.Message)")
|
}
|
|
$ComputeQueryStatus = "ok"
|
$ComputeRows = @(& nvidia-smi --query-compute-apps=pid,used_gpu_memory --format=csv,noheader,nounits 2>&1)
|
$ComputeExitCode = $LASTEXITCODE
|
$ComputeRawRows = [string](ConvertTo-Json -Compress -InputObject @($ComputeRows | ForEach-Object { [string]$_ }))
|
$ComputeTaskRows = New-Object System.Collections.Generic.List[string]
|
if ($ComputeExitCode -ne 0) {
|
$ComputeQueryStatus = "error"
|
[void]$SamplingErrors.Add("$($Now.ToString('o')) nvidia-smi compute query failed: exit=$ComputeExitCode rows=$ComputeRawRows")
|
} else {
|
foreach ($ComputeRow in $ComputeRows) {
|
if ([string]$ComputeRow -match '^\s*(\d+)\s*,') {
|
if ($TaskPids.Contains([int]$Matches[1])) { [void]$ComputeTaskRows.Add([string]$ComputeRow) }
|
}
|
}
|
}
|
|
$DedicatedBytes = 0.0
|
$GpuProcessMemoryStatus = "ok"
|
$GpuProcessInstances = New-Object System.Collections.Generic.List[object]
|
try {
|
if ($null -eq $GpuMemoryCategory) { throw "GPU Process Memory category unavailable" }
|
foreach ($Instance in @($GpuMemoryCategory.GetInstanceNames())) {
|
if ([string]$Instance -notmatch '(?i)(?:^|_)pid_(\d+)(?:_|$)') { continue }
|
$CounterPid = [int]$Matches[1]
|
if (-not $TaskPids.Contains($CounterPid)) { continue }
|
$CounterPath = "\GPU Process Memory($Instance)\Dedicated Usage"
|
$Counter = New-Object Diagnostics.PerformanceCounter("GPU Process Memory", "Dedicated Usage", [string]$Instance, $true)
|
try {
|
$CounterValue = [double]$Counter.NextValue()
|
$DedicatedBytes += $CounterValue
|
[void]$GpuProcessInstances.Add([ordered]@{
|
pid = $CounterPid
|
instance_name = [string]$Instance
|
counter_path = $CounterPath
|
dedicated_usage_bytes = [int64]$CounterValue
|
})
|
} finally { $Counter.Dispose() }
|
}
|
} catch {
|
$GpuProcessMemoryStatus = "error"
|
[void]$SamplingErrors.Add("$($Now.ToString('o')) GPU Process Memory query failed: $($_.Exception.Message)")
|
}
|
|
if ($InjectSampleFailureOnce -and $ScanCloseCommitted -and $InjectedSampleFailureCount -eq 0) {
|
$InjectedSampleFailureCount = 1
|
throw "injected post-scan sample failure before CSV commit"
|
}
|
|
$RootPriorityRecord = $Priorities | Where-Object { $_.pid -eq $RootPid } | Select-Object -First 1
|
if ($null -eq $RootPriorityRecord) {
|
$Process.Refresh()
|
if (-not $Process.HasExited) {
|
throw "Root process priority unavailable while PID $RootPid is still running"
|
}
|
$RootPriorityText = "Exited"
|
} else {
|
$RootPriorityText = [string]$RootPriorityRecord.priority
|
}
|
|
[void]$Samples.Add([pscustomobject][ordered]@{
|
timestamp_utc = $Now.ToString("o")
|
previous_valid_timestamp_utc = $PreviousText
|
interval_seconds = [Math]::Round($Interval, 6)
|
counter_warmup = $Warmup
|
cpu_valid = $CpuValid
|
host_cpu_percent = if ($CpuValid) { [Math]::Round($HostCpu, 6) } else { "" }
|
scan_phase_active = $ScanPhaseActive
|
scan_ffmpeg_present = $ScanPresent
|
scan_ffmpeg_exit_observed = $ScanExitObserved
|
scan_ffmpeg_pid = if ($null -eq $ScanPid) { "" } else { [int]$ScanPid }
|
scan_ffmpeg_command_line = $ScanCommandLine
|
decoder = $ScanDecoder
|
logical_processor_count = $LogicalProcessorCount
|
task_pids = (($TaskPids | Sort-Object) -join ";")
|
task_cpu_percent = if ($CpuValid) { [Math]::Round($TaskCpu, 6) } else { "" }
|
task_working_set_bytes = $WorkingSet
|
root_priority = $RootPriorityText
|
ffmpeg_priorities = [string](ConvertTo-Json -Compress -InputObject @(
|
$Priorities | Where-Object { $FfmpegPids.Contains([int]$_.pid) }
|
))
|
ffmpeg_command_lines = [string](ConvertTo-Json -Compress -InputObject @(
|
$CommandLinesByPid.GetEnumerator() | ForEach-Object {
|
[ordered]@{ pid=[int]$_.Key; command_line=[string]$_.Value }
|
}
|
))
|
gpu_query_status = $GpuQueryStatus
|
gpu_query_raw_rows = $GpuRawRows
|
gpu_utilization_percent = if ([double]::IsNaN($GpuUtil)) { "" } else { $GpuUtil }
|
gpu_decoder_utilization_percent = if ([double]::IsNaN($DecoderUtil)) { "" } else { $DecoderUtil }
|
gpu_memory_used_mib = if ([double]::IsNaN($GpuMemory)) { "" } else { $GpuMemory }
|
nvidia_smi_compute_query_status = $ComputeQueryStatus
|
nvidia_smi_compute_raw_rows = $ComputeRawRows
|
nvidia_smi_compute_task_rows = [string](ConvertTo-Json -Compress -InputObject $ComputeTaskRows.ToArray())
|
gpu_process_memory_query_status = $GpuProcessMemoryStatus
|
gpu_process_memory_instances = [string](ConvertTo-Json -Compress -InputObject $GpuProcessInstances.ToArray())
|
task_gpu_dedicated_memory_mib = [Math]::Round($DedicatedBytes / 1MB, 6)
|
})
|
$PreviousCounterTimestamp = $Now
|
$FirstCounter = $false
|
if ($ScanExitObserved) {
|
$ScanCloseCommitted = $true
|
}
|
} catch {
|
[void]$SamplingErrors.Add(
|
"$([DateTime]::UtcNow.ToString('o')) $($_.Exception.Message) at $($_.ScriptStackTrace)"
|
)
|
}
|
|
$Process.Refresh()
|
if (-not $Process.HasExited) {
|
$LastSchedule += $SampleIntervalMilliseconds
|
if ($LastSchedule -le $Stopwatch.ElapsedMilliseconds) {
|
$LastSchedule = $Stopwatch.ElapsedMilliseconds + 1
|
}
|
Start-Sleep -Milliseconds ([int][Math]::Max(1, $LastSchedule - $Stopwatch.ElapsedMilliseconds))
|
}
|
} while (-not $Process.HasExited)
|
|
$Process.WaitForExit()
|
$StdoutText = [string]$StdoutTask.GetAwaiter().GetResult()
|
$StderrText = [string]$StderrTask.GetAwaiter().GetResult()
|
[System.IO.File]::WriteAllText($StdoutPath, $StdoutText, $Utf8NoBom)
|
[System.IO.File]::WriteAllText($StderrPath, $StderrText, $Utf8NoBom)
|
$HostCpuCounter.Dispose()
|
$Stopwatch.Stop()
|
$EndUtc = [DateTime]::UtcNow
|
$Samples | Export-Csv -LiteralPath $SamplesPath -NoTypeInformation -Encoding UTF8
|
$Analysis = Analyze-ResourceSamples $SamplesPath $EvidenceDirectory
|
|
$SourceAfter = Get-SourceSnapshot $VideoPath
|
Write-JsonFile $SourceAfterPath $SourceAfter
|
$SourceBefore = Get-Content -LiteralPath $SourceBeforePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
$SourceUnchanged = (
|
[int64]$SourceBefore.size_bytes -eq [int64]$SourceAfter.size_bytes -and
|
[string]$SourceBefore.creation_time_utc -eq [string]$SourceAfter.creation_time_utc -and
|
[string]$SourceBefore.last_write_time_utc -eq [string]$SourceAfter.last_write_time_utc -and
|
[string]$SourceBefore.sha256 -eq [string]$SourceAfter.sha256
|
)
|
$AllPrioritiesBelowNormal = @($Samples | Where-Object {
|
$_.scan_ffmpeg_present -and (
|
$_.root_priority -ne "BelowNormal" -or
|
$_.ffmpeg_priorities -match '"priority":"(?!BelowNormal)'
|
)
|
}).Count -eq 0
|
$NvdecLogEvidence = $false
|
$ExpectedNvdecLog = ""
|
if (-not [string]::IsNullOrWhiteSpace($ScanDecoder)) {
|
$NormalizedScanDecoder = ([string]$ScanDecoder).Trim().ToLowerInvariant()
|
$ExpectedNvdecLog = "NVDEC/CUDA ($NormalizedScanDecoder)"
|
$PersistedStdoutText = [IO.File]::ReadAllText($StdoutPath, [Text.Encoding]::UTF8)
|
$NvdecLogEvidence = $PersistedStdoutText.Contains($ExpectedNvdecLog)
|
}
|
$NvdecCommandEvidence = Test-NvdecScanCommand $ScanCommandLine $ScanDecoder
|
$SamplingValid = (
|
$Analysis.Summary.sampling_valid -and
|
$SamplingErrors.Count -eq 0 -and
|
$ScanFfmpegSeen -and
|
-not $MultipleScanPidsSeen -and
|
$ScanPhaseClosed -and
|
$AllPrioritiesBelowNormal
|
)
|
$ResourcePass = (
|
[int]$Process.ExitCode -eq 0 -and
|
$SamplingValid -and
|
$Analysis.Summary.cpu_gate_pass -and
|
$NvdecLogEvidence -and
|
$NvdecCommandEvidence -and
|
$SourceUnchanged
|
)
|
$RunSummary = [ordered]@{
|
generated_at_utc = [DateTime]::UtcNow.ToString("o")
|
target_exit_code = [int]$Process.ExitCode
|
elapsed_seconds = [Math]::Round($Stopwatch.Elapsed.TotalSeconds, 3)
|
root_python_pid = $RootPid
|
sample_count = $Samples.Count
|
sampling_errors = @($SamplingErrors)
|
command_line_disappearance_race_count = $CommandLineDisappearanceRaceCount
|
injected_sample_failure_count = $InjectedSampleFailureCount
|
sampling_valid = $SamplingValid
|
cpu_gate_pass = $Analysis.Summary.cpu_gate_pass
|
scan_ffmpeg_seen = $ScanFfmpegSeen
|
scan_ffmpeg_pid = $ScanPid
|
scan_ffmpeg_command_line = $ScanCommandLine
|
decoder = $ScanDecoder
|
all_scan_priorities_below_normal = $AllPrioritiesBelowNormal
|
nvdec_command_log_evidence = $NvdecLogEvidence
|
nvdec_expected_log = $ExpectedNvdecLog
|
nvdec_scan_argv_evidence = $NvdecCommandEvidence
|
valid_scan_sample_count = $Analysis.Summary.valid_scan_sample_count
|
longest_high_cpu_segment = $Analysis.Summary.longest_high_cpu_segment
|
task_cpu_peak_percent = $Analysis.Summary.task_cpu_peak_percent
|
task_working_set_peak_bytes = $Analysis.Summary.task_working_set_peak_bytes
|
gpu_utilization_peak_percent = $Analysis.Summary.gpu_utilization_peak_percent
|
gpu_decoder_utilization_peak_percent = $Analysis.Summary.gpu_decoder_utilization_peak_percent
|
gpu_memory_used_peak_mib = $Analysis.Summary.gpu_memory_used_peak_mib
|
task_gpu_dedicated_memory_peak_mib = $Analysis.Summary.task_gpu_dedicated_memory_peak_mib
|
source_unchanged = $SourceUnchanged
|
resource_acceptance_pass = $ResourcePass
|
}
|
Write-JsonFile (Join-Path $EvidenceDirectory "run_summary.json") $RunSummary
|
Write-JsonFile $CommandPath ([ordered]@{
|
executable = $PythonPath
|
arguments = $Arguments
|
command_line = "$PythonPath $ArgumentString"
|
working_directory = (Get-Location).Path
|
start_time_utc = $StartUtc.ToString("o")
|
end_time_utc = $EndUtc.ToString("o")
|
exit_code = [int]$Process.ExitCode
|
})
|
Write-Output "target_exit_code=$([int]$Process.ExitCode)"
|
Write-Output "sampling_valid=$SamplingValid"
|
Write-Output "cpu_gate_pass=$($Analysis.Summary.cpu_gate_pass)"
|
Write-Output "resource_acceptance_pass=$ResourcePass"
|
if ([int]$Process.ExitCode -ne 0) {
|
exit ([int]$Process.ExitCode)
|
}
|
if (-not $ResourcePass) {
|
exit 3
|
}
|
exit 0
|