|
| 1 | +param( |
| 2 | + [Parameter(Mandatory=$true)][string]$Command |
| 3 | +) |
| 4 | +$ErrorActionPreference='Stop' |
| 5 | + |
| 6 | +function Ensure-Tool($tool,$install){ |
| 7 | + if(-not (Get-Command $tool -ErrorAction SilentlyContinue)){ |
| 8 | + Write-Host "Installing missing tool: $tool" -ForegroundColor Yellow |
| 9 | + & dotnet tool install --global $install |
| 10 | + if($LASTEXITCODE -ne 0){ throw "Failed to install tool $tool" } |
| 11 | + } |
| 12 | +} |
| 13 | + |
| 14 | +${script:DotNetOnly} = $false |
| 15 | +function Select-Pid($title){ |
| 16 | + Import-Module PwshSpectreConsole -ErrorAction Stop |
| 17 | + $procs = Get-Process | Where-Object { $_.Id -gt 0 } |
| 18 | + if($script:DotNetOnly){ $procs = $procs | Where-Object { $_.ProcessName -match 'dotnet|Presentation.Web.Server' } } |
| 19 | + $procs = $procs | Sort-Object ProcessName,Id |
| 20 | + $rows = @() |
| 21 | + foreach($p in $procs){ |
| 22 | + $label = "$($p.ProcessName) (#$($p.Id))" |
| 23 | + if($rows -notcontains $label){ $rows += $label } |
| 24 | + } |
| 25 | + if(-not $rows){ Write-Host 'No matching processes found.' -ForegroundColor Yellow; return $null } |
| 26 | + $choices = $rows + 'Cancel' |
| 27 | + $sel = Read-SpectreSelection -Title $title -Choices $choices -EnableSearch -PageSize 25 |
| 28 | + Write-Host "Raw selection: '$sel'" -ForegroundColor DarkGray |
| 29 | + if([string]::IsNullOrWhiteSpace($sel) -or $sel -eq 'Cancel'){ return $null } |
| 30 | + if($sel -match '\(#(\d+)\)$'){ Write-Host "Selected PID: $($Matches[1])" -ForegroundColor DarkGray; return [int]$Matches[1] } |
| 31 | + Write-Host "Could not parse PID from selection: $sel" -ForegroundColor Yellow |
| 32 | + return $null |
| 33 | +} |
| 34 | + |
| 35 | +switch($Command.ToLowerInvariant()){ |
| 36 | + 'bench' { |
| 37 | + # Run benchmark project if exists |
| 38 | + $benchProj = Get-ChildItem -Recurse -Filter '*Benchmarks.csproj' | Select-Object -First 1 |
| 39 | + if(-not $benchProj){ Write-Host 'No benchmark project (*.Benchmarks.csproj) found.' -ForegroundColor Yellow; break } |
| 40 | + Write-Host "Attempting benchmark run: $($benchProj.FullName)" -ForegroundColor Cyan |
| 41 | + try { |
| 42 | + $env:DOTNET_EnableDiagnostics=0 |
| 43 | + & dotnet run --project $benchProj.FullName -c Release -- --filter '*' --anyCategories "*" |
| 44 | + Remove-Item Env:DOTNET_EnableDiagnostics -ErrorAction SilentlyContinue |
| 45 | + if($LASTEXITCODE -ne 0){ throw 'BenchmarkDotNet failed' } |
| 46 | + Write-Host 'Benchmarks completed.' -ForegroundColor Green |
| 47 | + } |
| 48 | + catch { |
| 49 | + Write-Host "Benchmark run failed: $($_.Exception.Message). Falling back to simple performance smoke (build + run)." -ForegroundColor Yellow |
| 50 | + Remove-Item Env:DOTNET_EnableDiagnostics -ErrorAction SilentlyContinue |
| 51 | + & dotnet build $benchProj.FullName -c Release |
| 52 | + if($LASTEXITCODE -ne 0){ Write-Host 'Fallback build failed.' -ForegroundColor Red; break } |
| 53 | + & dotnet run --project $benchProj.FullName -c Release --no-build |
| 54 | + Write-Host 'Fallback benchmark smoke complete.' -ForegroundColor Green |
| 55 | + } |
| 56 | + } |
| 57 | + 'trace-flame' { |
| 58 | + Ensure-Tool 'dotnet-counters' 'dotnet-counters' |
| 59 | + Ensure-Tool 'dotnet-trace' 'dotnet-trace' |
| 60 | + $script:DotNetOnly = $true |
| 61 | + $procId = Select-Pid 'Select process to trace (flame)' |
| 62 | + if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break } |
| 63 | + $outDir = Join-Path $PSScriptRoot '..' '.tmp' 'diagnostics' |
| 64 | + New-Item -ItemType Directory -Force -Path $outDir | Out-Null |
| 65 | + $fileBase = "trace_${procId}_$(Get-Date -Format 'yyyyMMdd_HHmmss')" |
| 66 | + $traceFile = Join-Path $outDir "$fileBase.nettrace" |
| 67 | + $speedFile = Join-Path $outDir "$fileBase.speedscope.json" |
| 68 | + Write-Host "Collecting trace for PID $procId ..." -ForegroundColor Cyan |
| 69 | + & dotnet-trace collect --process-id $procId --providers Microsoft-DotNETCore-SampleProfiler:1 --duration 00:00:10 -o $traceFile |
| 70 | + if($LASTEXITCODE -ne 0){ |
| 71 | + Write-Host 'SampleProfiler provider failed, retrying with default trace config (cpu+gc)...' -ForegroundColor Yellow |
| 72 | + & dotnet-trace collect --process-id $procId --duration 00:00:10 -o $traceFile |
| 73 | + if($LASTEXITCODE -ne 0){ throw 'Trace collection failed (fallback also failed)' } |
| 74 | + } |
| 75 | + Write-Host 'Converting to speedscope...' -ForegroundColor Cyan |
| 76 | + & dotnet-trace convert --format SpeedScope $traceFile -o $speedFile |
| 77 | + if($LASTEXITCODE -ne 0){ throw 'Trace conversion failed' } |
| 78 | + Write-Host "Trace complete: $traceFile" -ForegroundColor Green |
| 79 | + Write-Host "Speedscope file: $speedFile" -ForegroundColor Green |
| 80 | + } |
| 81 | + 'dump-heap' { |
| 82 | + Ensure-Tool 'dotnet-dump' 'dotnet-dump' |
| 83 | + $script:DotNetOnly = $true |
| 84 | + $procId = Select-Pid 'Select process for heap dump' |
| 85 | + if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break } |
| 86 | + $outDir = Join-Path $PSScriptRoot '..' '.tmp' 'diagnostics' |
| 87 | + New-Item -ItemType Directory -Force -Path $outDir | Out-Null |
| 88 | + $dumpFile = Join-Path $outDir "heap_${procId}_$(Get-Date -Format 'yyyyMMdd_HHmmss').dmp" |
| 89 | + Write-Host "Creating heap dump for PID $procId ..." -ForegroundColor Cyan |
| 90 | + & dotnet-dump collect --process-id $procId --type full -o $dumpFile |
| 91 | + if($LASTEXITCODE -ne 0){ throw 'Heap dump failed' } |
| 92 | + Write-Host "Heap dump created: $dumpFile" -ForegroundColor Green |
| 93 | + } |
| 94 | + 'gc-stats' { # Collect GC stats via dotnet-counters https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-9.0#view-metrics-with-dotnet-counters |
| 95 | + Ensure-Tool 'dotnet-counters' 'dotnet-counters' |
| 96 | + $script:DotNetOnly = $true |
| 97 | + $procId = Select-Pid 'Select process for GC stats' |
| 98 | + if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break } |
| 99 | + Write-Host "Sampling GC counters for PID $procId (5s) ..." -ForegroundColor Cyan |
| 100 | + # & dotnet-counters monitor --process-id $procId --counters "System.Runtime[gc-heap-size;time-in-gc]" --refresh-interval 1 --duration 5 |
| 101 | + & dotnet-counters monitor --process-id $procId --counters "System.Runtime" --refresh-interval 1 --duration 5 |
| 102 | + if($LASTEXITCODE -ne 0){ throw 'GC stats collection failed' } |
| 103 | + Write-Host 'GC sampling complete.' -ForegroundColor Green |
| 104 | + } |
| 105 | + 'aspnet-metrics' { |
| 106 | + Ensure-Tool 'dotnet-counters' 'dotnet-counters' |
| 107 | + $script:DotNetOnly = $true |
| 108 | + $procId = Select-Pid 'Select ASP.NET Core process for metrics' |
| 109 | + if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break } |
| 110 | + Write-Host "Monitoring ASP.NET Core + runtime counters for PID $procId (10s) ..." -ForegroundColor Cyan |
| 111 | + # $counterGroups = @( |
| 112 | + # 'Microsoft.AspNetCore.Hosting[requests-started;requests-completed;current-requests]', |
| 113 | + # 'Microsoft.AspNetCore.Server.Kestrel[connection-queue-length;connections-active;connections-opened;connections-closed]', |
| 114 | + # 'System.Net.Http[requests-started;requests-failed]', |
| 115 | + # 'System.Runtime[cpu-usage;working-set;gc-heap-size;gen-0-gc-count;gen-1-gc-count;gen-2-gc-count;time-in-gc]' |
| 116 | + # ) |
| 117 | + $counterGroups = @( |
| 118 | + 'Microsoft.AspNetCore.Hosting' |
| 119 | + ) |
| 120 | + $countersArg = $counterGroups -join ' ' |
| 121 | + & dotnet-counters monitor --process-id $procId --counters $countersArg --refresh-interval 1 --duration 10 |
| 122 | + if($LASTEXITCODE -ne 0){ throw 'ASP.NET metrics collection failed' } |
| 123 | + Write-Host 'ASP.NET metrics sampling complete.' -ForegroundColor Green |
| 124 | + } |
| 125 | + default { throw "Unknown diagnostics command: $Command" } |
| 126 | +} |
0 commit comments