param( [Parameter(Mandatory)][string]$OutputDirectory, [ValidateRange(5,900)][int]$Seconds=60, [string]$ProbeExecutable, [string[]]$ProbeArguments=@(), [ValidateRange(0,9223372036854775807)][long]$ProbeAffinityMask=0 ) # Runs an owned, uniquely named ETW session. Does not launch or stop Dolphin. # Native use requires a separately launched, frozen diagnostic build and manifest. $ErrorActionPreference = 'Stop' function ConvertTo-P1WindowsArgument([string]$Value) { # Windows PowerShell 5.1 uses .NET Framework, where ProcessStartInfo has no # ArgumentList. Quote paths using the standard Windows argv backslash rules. $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)) + '"' } $p1Out = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path (Get-Location).ProviderPath $OutputDirectory })) if (Test-Path -LiteralPath $p1Out) { throw 'Use a NEW capture directory; results are never overwritten.' } if ($ProbeExecutable) { $ProbeExecutable = (Resolve-Path -LiteralPath $ProbeExecutable).Path if ([IO.Path]::GetFileName($ProbeExecutable) -ne 'TraceProbeP1.exe') { throw 'Automatic launch is restricted to the local qualification probe, not Dolphin.' } } 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." } } $p1Drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($p1Out)) if ($p1Drive.AvailableFreeSpace -lt 9GB) { throw 'Need 4 GiB trace budget plus 5 GiB reserve.' } New-Item -ItemType Directory -Path $p1Out | Out-Null $p1Name = 'DolphinP1-' + [guid]::NewGuid().ToString('N') $p1Temp = Join-Path (Split-Path $p1Out -Parent) ($p1Name + '-temporary-etl') New-Item -ItemType Directory -Path $p1Temp | Out-Null $p1Profile = Join-Path $PSScriptRoot 'DolphinP1.wprp' $p1ETL = Join-Path $p1Out 'capture.etl' $p1PDB = if ($ProbeExecutable) { Join-Path (Split-Path (Split-Path (Split-Path $ProbeExecutable -Parent) -Parent) -Parent) 'pdb/TraceProbeP1.pdb' } else { $null } $p1Started = $false $p1Reason = 'duration' $p1Probe = $null $p1Stdout = $null $p1Stderr = $null $p1Manifest = [ordered]@{ Schema=1; Collection='WPRSystemAndNative'; Session=$p1Name StartUTC=[datetime]::UtcNow.ToString('o') Seconds=$Seconds; TemporaryTraceDirectory=$p1Temp Profile=$p1Profile; ProfileSHA256=(Get-FileHash -LiteralPath $p1Profile).Hash Probe=$ProbeExecutable; ProbeSHA256=$(if ($ProbeExecutable) { (Get-FileHash -LiteralPath $ProbeExecutable).Hash } else { $null }) ProbeAffinityMask=$ProbeAffinityMask ProbePDB=$p1PDB; ProbePDBSHA256=$(if ($p1PDB -and (Test-Path -LiteralPath $p1PDB)) { (Get-FileHash -LiteralPath $p1PDB).Hash } else { $null }) NativeQualificationPassed=$false } $p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Out 'manifest.json') -Encoding utf8 try { & wpr.exe -start ($p1Profile + '!DolphinP1') -filemode -recordtempto $p1Temp -instancename $p1Name *> (Join-Path $p1Out 'start.log') if ($LASTEXITCODE) { throw "ETW start failed; see $p1Out\start.log. No other session was stopped." } $p1Started = $true if (-not $ProbeExecutable) { Write-Output "Native collector armed for $Seconds seconds: $p1Name. Launch Dolphin separately with -PerformanceTrace now." } if ($ProbeExecutable) { # Environment of this child only; never persistently modify the user's environment. $p1Info = [Diagnostics.ProcessStartInfo]::new($ProbeExecutable) $p1Info.UseShellExecute = $false $p1Info.CreateNoWindow = $true $p1Info.RedirectStandardOutput = $true $p1Info.RedirectStandardError = $true $p1Info.Environment['DOLPHIN_PERF_TRACE'] = '1' if ($p1Info.PSObject.Properties['ArgumentList'] -and $p1Info.ArgumentList) { foreach ($p1Arg in $ProbeArguments) { $p1Info.ArgumentList.Add($p1Arg) } } else { $p1Info.Arguments = ($ProbeArguments | ForEach-Object { ConvertTo-P1WindowsArgument $_ }) -join ' ' } $p1Probe = [Diagnostics.Process]::Start($p1Info) if ($ProbeAffinityMask -ne 0) { try { $p1Probe.ProcessorAffinity = [IntPtr]$ProbeAffinityMask } catch { $p1Probe.Kill() # Only the synthetic probe started by this script. $p1Probe.WaitForExit() throw "Could not pin owned synthetic probe to mask ${ProbeAffinityMask}: $_" } } $p1Manifest.ProbeObservedAffinityMask = $p1Probe.ProcessorAffinity.ToInt64() $p1Stdout = $p1Probe.StandardOutput.ReadToEndAsync() $p1Stderr = $p1Probe.StandardError.ReadToEndAsync() $p1Manifest.ProbePID = $p1Probe.Id $p1Manifest.ProbeStartUTC = $p1Probe.StartTime.ToUniversalTime().ToString('o') } $p1Clock = [Diagnostics.Stopwatch]::StartNew() while ($p1Clock.Elapsed.TotalSeconds -lt $Seconds) { Start-Sleep -Milliseconds 1000 $p1Bytes = (Get-ChildItem -LiteralPath $p1Temp -File -Recurse | Measure-Object Length -Sum).Sum if ($p1Drive.AvailableFreeSpace -lt 5GB -or $p1Bytes -gt 4GB) { $p1Reason='storage-budget'; break } if ($p1Probe -and $p1Probe.HasExited) { $p1Reason='probe-exit'; break } } } finally { if ($p1Started) { & wpr.exe -status collectors -details -instancename $p1Name *> (Join-Path $p1Out 'collector-status.log') & wpr.exe -stop $p1ETL -instancename $p1Name *> (Join-Path $p1Out 'stop.log') $p1StopExit = $LASTEXITCODE $p1Manifest.EndUTC = [datetime]::UtcNow.ToString('o') $p1Manifest.StopReason = $p1Reason $p1Manifest.StopExitCode = $p1StopExit $p1Manifest.ProbeExitCode = if ($p1Probe -and $p1Probe.HasExited) { $p1Probe.ExitCode } else { $null } if ($p1Probe -and $p1Probe.HasExited) { $p1Stdout.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Out 'probe.stdout.log') -Encoding utf8 $p1Stderr.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Out 'probe.stderr.log') -Encoding utf8 $p1Manifest.ProbeCPUSeconds = $p1Probe.TotalProcessorTime.TotalSeconds $p1Manifest.ProbeWallSeconds = ($p1Probe.ExitTime - $p1Probe.StartTime).TotalSeconds } $p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Out 'manifest.json') -Encoding utf8 if ($p1StopExit) { throw 'Owned ETW session could not be saved. See stop.log; no broad cancel attempted.' } } } if (-not (Test-Path -LiteralPath $p1ETL)) { throw 'WPR reported success but produced no capture.etl.' } if ($p1Probe -and -not $p1Probe.HasExited) { throw "Capture stopped at $p1Reason, but the owned local probe is still running (PID $($p1Probe.Id)). No process was forcibly stopped." } if ($p1Probe -and $p1Probe.ExitCode -ne 0) { throw "Local probe failed with exit code $($p1Probe.ExitCode); see probe.stderr.log. The ETL was kept." } Write-Output "Saved $p1ETL. Qualification is NOT automatically passed; inspect losses, attribution and overhead."