param( [Parameter(Mandatory)][string]$OutputDirectory, [ValidateRange(30,300)][int]$TimeoutSeconds=120, [ValidateRange(0,9223372036854775807)][long]$ProbeAffinityMask=0, [switch]$NoTrace ) # Synthetic user-mode ETW qualification. This does not sample CPU or scheduling. $ErrorActionPreference='Stop' 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 '../..')) if ($ProbeAffinityMask -ne 0) { $p1Allowed = [Diagnostics.Process]::GetCurrentProcess().ProcessorAffinity.ToInt64() if (($ProbeAffinityMask -band $p1Allowed) -ne $ProbeAffinityMask) { throw "Probe affinity mask $ProbeAffinityMask is not available to this process." } } $p1Output=[IO.Path]::GetFullPath($(if([IO.Path]::IsPathRooted($OutputDirectory)){ $OutputDirectory }else{ Join-Path (Get-Location).ProviderPath $OutputDirectory })) $p1Probe=Join-Path $p1Repo '.starlet_diag_p1/Binaries/Tests/TraceProbeP1.exe' $p1PDB=Join-Path $p1Repo '.starlet_diag_p1/pdb/TraceProbeP1.pdb' $p1Network=Join-Path $p1Repo '.starlet_diag_p1/Binaries/Network' $p1Decoder=Join-Path $p1Repo '.starlet-profile/P1-tools/DecodeP1.exe' foreach($p1Input in @($p1Probe,$p1PDB,$p1Network,$p1Decoder)) { if(-not (Test-Path -LiteralPath $p1Input)) { throw "Missing local qualification dependency: $p1Input" } } if(Test-Path -LiteralPath $p1Output) { throw 'Use a NEW directory; trace evidence is never overwritten.' } New-Item -ItemType Directory -Path $p1Output | Out-Null $p1Journal=Join-Path $p1Output 'synthetic.journal' $p1ETL=Join-Path $p1Output 'native.etl' $p1Events=Join-Path $p1Output 'events.jsonl' $p1Info=[Diagnostics.ProcessStartInfo]::new($p1Probe) $p1Info.UseShellExecute=$false $p1Info.CreateNoWindow=$true $p1Info.RedirectStandardOutput=$true $p1Info.RedirectStandardError=$true $p1Info.WorkingDirectory=$p1Repo if($NoTrace) { $p1Info.Environment.Remove('DOLPHIN_PERF_TRACE') | Out-Null } else { $p1Info.Environment['DOLPHIN_PERF_TRACE']='1' } $p1Arguments=if($NoTrace) { @($p1Journal,$p1Network) } else { @($p1Journal,$p1Network,$p1ETL) } if($p1Info.PSObject.Properties['ArgumentList'] -and $p1Info.ArgumentList) { foreach($p1Arg in $p1Arguments) { $p1Info.ArgumentList.Add($p1Arg) } } else { $p1Info.Arguments=($p1Arguments | ForEach-Object { ConvertTo-P1WindowsArgument $_ }) -join ' ' } $p1Process=[Diagnostics.Process]::Start($p1Info) if ($ProbeAffinityMask -ne 0) { try { $p1Process.ProcessorAffinity = [IntPtr]$ProbeAffinityMask } catch { $p1Process.Kill() # Only the synthetic probe started by this script. $p1Process.WaitForExit() throw "Could not pin owned synthetic probe to mask ${ProbeAffinityMask}: $_" } } $p1ObservedAffinity = $p1Process.ProcessorAffinity.ToInt64() $p1Stdout=$p1Process.StandardOutput.ReadToEndAsync() $p1Stderr=$p1Process.StandardError.ReadToEndAsync() if(-not $p1Process.WaitForExit($TimeoutSeconds*1000)) { throw "Local probe exceeded $TimeoutSeconds seconds. Its PID is $($p1Process.Id); no process was forcibly stopped." } $p1Stdout.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Output 'probe.stdout.log') -Encoding utf8 $p1Stderr.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Output 'probe.stderr.log') -Encoding utf8 $p1Manifest=[ordered]@{ Schema=1; Collection=$(if($NoTrace){'NoTrace'}else{'PrivateUserMode'}); Probe=$p1Probe ProbeSHA256=(Get-FileHash -LiteralPath $p1Probe).Hash ProbePDB=$p1PDB; ProbePDBSHA256=(Get-FileHash -LiteralPath $p1PDB).Hash ProbePID=$p1Process.Id; ProbeExitCode=$p1Process.ExitCode ProbeAffinityMask=$ProbeAffinityMask; ProbeObservedAffinityMask=$p1ObservedAffinity ProbeStartUTC=$p1Process.StartTime.ToUniversalTime().ToString('o') ProbeEndUTC=$p1Process.ExitTime.ToUniversalTime().ToString('o') ProbeWallSeconds=($p1Process.ExitTime-$p1Process.StartTime).TotalSeconds ProbeCPUSeconds=$p1Process.TotalProcessorTime.TotalSeconds NativeQualificationPassed=$false } $p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Output 'manifest.json') -Encoding utf8 if($p1Process.ExitCode -ne 0) { throw 'Local probe failed; see probe.stderr.log.' } if($NoTrace) { Write-Output "No-trace fixed-work baseline completed: $p1Output" return } if(-not (Test-Path -LiteralPath $p1ETL)) { throw 'Local probe produced no ETL.' } $p1DecodeText=& $p1Decoder $p1ETL $p1Events $p1DecodeExit=$LASTEXITCODE $p1DecodeText | Out-File -LiteralPath (Join-Path $p1Output 'decoder-summary.json') -Encoding utf8 if($p1DecodeExit) { throw 'ETL decode/loss check failed; see decoder-summary.json.' } Write-Output "Private user-mode trace decoded: $p1Output" Write-Output 'This capture has no CPU samples, context switches or disk wait events.'