param( [Parameter(Mandatory)][string]$CaptureDirectory, [Parameter(Mandatory)][string]$OutputDirectory, [string]$PythonExecutable, [ValidateRange(0,2147483647)][int]$NativePID=0, [string]$NativePDB, [string]$XperfRangeStart, [string]$XperfRangeEnd ) # Read-only input: qualify a synthetic capture, or inspect one native Dolphin PID. # Output is a new directory; no emulator, NAND or WPR session is touched. $ErrorActionPreference = 'Stop' if (($NativePID -eq 0) -ne (-not $NativePDB)) { throw 'For native analysis pass both -NativePID and -NativePDB.' } if ([bool]$XperfRangeStart -ne [bool]$XperfRangeEnd) { throw 'Pass both -XperfRangeStart and -XperfRangeEnd, or neither.' } if ($XperfRangeStart -and -not $NativePID) { throw 'Time-window export is only supported for exploratory native analysis.' } if ($PythonExecutable) { $p1Python = (Resolve-Path -LiteralPath $PythonExecutable).Path } else { $p1Candidates = @(Get-Command python.exe -All -ErrorAction SilentlyContinue | Where-Object { $_.Source -notlike '*\WindowsApps\*' } | Select-Object -ExpandProperty Source) $p1Candidates += 'C:\msys64\mingw64\bin\python.exe' foreach ($p1Candidate in $p1Candidates) { if ($p1Candidate -and (Test-Path -LiteralPath $p1Candidate)) { $p1Python = $p1Candidate break } } } if (-not $p1Python) { throw 'Python was not found before analysis. Pass -PythonExecutable with the path to a working python.exe.' } & $p1Python --version | Out-Null if ($LASTEXITCODE) { throw "Python could not start: $p1Python" } function ConvertTo-P1WindowsArgument([string]$Value) { $p1Quoted = '"' $p1Slashes = 0 foreach ($p1Char in $Value.ToCharArray()) { if ($p1Char -eq '\') { ++$p1Slashes; continue } if ($p1Char -eq '"') { $p1Quoted += ('\' * (2 * $p1Slashes + 1)) + '"' $p1Slashes = 0 continue } $p1Quoted += ('\' * $p1Slashes) + $p1Char $p1Slashes = 0 } return $p1Quoted + ('\' * (2 * $p1Slashes)) + '"' } $p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) $p1Input = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($CaptureDirectory)) { $CaptureDirectory } else { Join-Path (Get-Location).ProviderPath $CaptureDirectory })) $p1Output = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $p1Repo $OutputDirectory })) if (Test-Path -LiteralPath $p1Output) { throw 'Use a NEW analysis directory; no results are overwritten.' } $p1ETL = Join-Path $p1Input 'capture.etl' $p1ManifestPath = Join-Path $p1Input 'manifest.json' $p1StdoutPath = Join-Path $p1Input 'probe.stdout.log' $p1Decoder = Join-Path $p1Repo '.starlet-profile/P1-tools/DecodeP1.exe' $p1RequiredInputs = @($p1ETL, $p1ManifestPath, $p1Decoder) if ($NativePID) { $NativePDB = (Resolve-Path -LiteralPath $NativePDB).Path $p1RequiredInputs += $NativePDB } else { $p1RequiredInputs += $p1StdoutPath } foreach ($p1Required in $p1RequiredInputs) { if (-not (Test-Path -LiteralPath $p1Required)) { throw "Missing analysis input: $p1Required" } } $p1Drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($p1Output)) $p1RequiredFree = if ($NativePID) { 20GB } else { 7GB } if ($p1Drive.AvailableFreeSpace -lt $p1RequiredFree) { throw "Need at least $([math]::Round($p1RequiredFree / 1GB)) GiB free for offline analysis." } New-Item -ItemType Directory -Path $p1Output | Out-Null Copy-Item -LiteralPath $p1ManifestPath -Destination $p1Output if (-not $NativePID) { Copy-Item -LiteralPath $p1StdoutPath -Destination $p1Output } $p1Manifest = Get-Content -LiteralPath $p1ManifestPath -Raw | ConvertFrom-Json $p1TargetPID = if ($NativePID) { $NativePID } else { $p1Manifest.ProbePID } if (-not $p1TargetPID) { throw 'Target PID missing from capture manifest.' } $p1Native = Join-Path $p1Output 'native-events.jsonl' $p1DecodeText = & $p1Decoder $p1ETL $p1Native $p1DecodeExit = $LASTEXITCODE $p1DecodeText | Out-File -LiteralPath (Join-Path $p1Output 'decoder-summary.json') -Encoding utf8 if ($p1DecodeExit) { throw 'TDH native decode or ETW loss check failed; see decoder-summary.json.' } $p1Csv = Join-Path $p1Output 'xperf-symbols.csv' $p1PriorSymbols = [Environment]::GetEnvironmentVariable('_NT_SYMBOL_PATH', 'Process') $p1PriorCache = [Environment]::GetEnvironmentVariable('_NT_SYMCACHE_PATH', 'Process') try { $env:_NT_SYMBOL_PATH = if ($NativePID) { Split-Path $NativePDB -Parent } else { Split-Path $p1Manifest.ProbePDB -Parent } $env:_NT_SYMCACHE_PATH = Join-Path $p1Output 'symcache' $p1Info = [Diagnostics.ProcessStartInfo]::new((Get-Command xperf.exe).Source) $p1Info.UseShellExecute = $false $p1Info.CreateNoWindow = $true $p1Info.RedirectStandardOutput = $true $p1Info.RedirectStandardError = $true $p1Info.WorkingDirectory = $p1Repo $p1XperfArgs = @('-i', $p1ETL, '-o', $p1Csv, '-symbols', '-target', 'machine', '-quiet', '-a', 'dumper') if ($XperfRangeStart) { $p1XperfArgs += @('-range', $XperfRangeStart, $XperfRangeEnd) [pscustomobject]@{ Start=$XperfRangeStart; End=$XperfRangeEnd; Note='Xperf CPU samples are limited to this window; native scopes and counters span the full ETL.' } | ConvertTo-Json | Out-File -LiteralPath (Join-Path $p1Output 'analysis-range.json') -Encoding utf8 } $p1Info.Arguments = ($p1XperfArgs | ForEach-Object { ConvertTo-P1WindowsArgument $_ }) -join ' ' $p1Process = [Diagnostics.Process]::Start($p1Info) $p1Stdout = $p1Process.StandardOutput.ReadToEndAsync() $p1Stderr = $p1Process.StandardError.ReadToEndAsync() $p1TimeoutMs = if ($NativePID) { 900000 } else { 180000 } if (-not $p1Process.WaitForExit($p1TimeoutMs)) { $p1Process.Kill() # Only the xperf process created by this analysis script. $p1Process.WaitForExit() throw "Owned Xperf decoder exceeded $($p1TimeoutMs / 1000) seconds; no input ETL was modified." } $p1Stdout.GetAwaiter().GetResult() | Out-File ` -LiteralPath (Join-Path $p1Output 'xperf-stdout.log') -Encoding utf8 $p1Stderr.GetAwaiter().GetResult() | Out-File ` -LiteralPath (Join-Path $p1Output 'xperf-warnings.log') -Encoding utf8 if ($p1Process.ExitCode) { throw 'Xperf kernel decode failed; see xperf-warnings.log.' } } finally { [Environment]::SetEnvironmentVariable('_NT_SYMBOL_PATH', $p1PriorSymbols, 'Process') [Environment]::SetEnvironmentVariable('_NT_SYMCACHE_PATH', $p1PriorCache, 'Process') } $p1Combined = Join-Path $p1Output 'combined.jsonl' $p1XperfSummary = Join-Path $p1Output 'xperf-summary.json' & $p1Python (Join-Path $PSScriptRoot 'xperf_to_p1.py') $p1Csv $p1Native ` --pid $p1TargetPID --output $p1Combined --summary $p1XperfSummary if ($LASTEXITCODE) { throw 'Xperf/native clock alignment failed.' } if ($NativePID) { & $p1Python (Join-Path $PSScriptRoot 'analyze_p1.py') $p1Combined ` --pid $p1TargetPID --output (Join-Path $p1Output 'native-report.json') if ($LASTEXITCODE) { throw 'Native offline analysis failed.' } Write-Output "Exploratory native report: $p1Output\native-report.json (NOT qualified as a complete Wii-update bottleneck report)." } else { & $p1Python (Join-Path $PSScriptRoot 'qualify_p1.py') $p1Output ` --events $p1Combined --xperf-summary $p1XperfSummary if ($LASTEXITCODE) { throw 'Local WPR qualification failed; see qualification.json.' } Write-Output "Local WPR qualification finished: $p1Output\qualification.json" }