Skip to content

Commit 795f9be

Browse files
davidfowlCopilot
andauthored
Add Windows CI resource telemetry to test and extension E2E jobs (#21644)
## Description Windows runner loss and hangs are the largest flake family in #21600: Windows extension E2E and .NET test jobs freeze at 47–59 minutes against a 3–9 minute baseline, then lose their runner (#21570). We can't currently answer whether the machines are overloaded on CPU, memory or disk, because nothing records machine load in a form that survives the job. This PR adds a lightweight background resource sampler to: - the Windows .NET test jobs (`run-tests.yml`) - the Windows VS Code extension E2E shards (`extension-e2e-tests.yml`) **What it records** (`eng/scripts/ci-resource-monitor.ps1`): a detached, below-normal-priority `pwsh` process starts right after checkout. Every 20s it writes: - `resources.csv`: CPU %, processor queue length, available MB, commit charge % / committed GB / commit limit, paging pages/s, page file usage %, disk queue length, disk % time, free GB on the system and workspace drives, and process count. - `processes.log`: the top 5 processes by CPU (since the previous sample) and by working set (with private bytes). **Where it goes:** the sampler writes into directories that the existing `if: always()` uploads already collect, so there are no new artifacts: - .NET tests: `artifacts/log/test-logs/resource-monitor/` inside `logs-<test>-windows-latest`. - Extension E2E: `extension/.test-results/resource-monitor/` inside `extension-e2e-diagnostics-win-x64-<shard>-attempt<N>`. A `Stop resource monitor (Windows)` step (`if: always()`) kills the sampler before the upload. It prints a peak/minimum table, including the largest gap between samples (a stall indicator), to the log and the job summary. This covers passing, failing, cancelled and timed-out jobs. **Safety:** - Every mode exits 0 and both steps are `continue-on-error`. - Test commands, timeouts, retries and environment are unchanged. - Linux and macOS are untouched. - Overhead is one `Get-Counter` and one `Get-Process` call every 20s at below-normal priority. **Limitation:** a runner that loses communication uploads nothing and keeps no logs. This telemetry can't capture the final minutes of a lost runner. It can show whether pressure builds on the slow jobs that survive and on the shards that typically get lost. That is still more than we have today: the existing console Heartbeat (60s, .NET tests only) vanishes with the log. Contributes to #21570. It complements #21608 (Docker pin), which adds no telemetry. ### Existing evidence (before this PR) On main, the last 16 completed `ci.yml` push runs had 1,551 Windows jobs. All 20 jobs that ran 45 minutes or longer were runner-lost; job logs return `BlobNotFound`, so there's no Heartbeat data from any of them. Examples: [extension E2E resource-debug-tools, 110m](https://github.com/microsoft/aspire/actions/runs/41283516743/job/124783549300) and [Hosting-4, 59m](https://github.com/microsoft/aspire/actions/runs/41168649739/job/124441989110). Surviving Windows Hosting-6 jobs on main ([example](https://github.com/microsoft/aspire/actions/runs/41215270228/job/124578892630)) show the following in Heartbeat: - memory 3.1–3.7 / 16.0 GB (20–23%) - CPU 60–80% during test execution - C: about 29 GB free, D: about 140 GB free None of this shows exhaustion, but it doesn't cover commit charge, page file or disk queue. ### Validation - `tests/Infrastructure.Tests/PowerShellScripts/CiResourceMonitorTests.cs`: - the summary math (peaks, minimums, averages, stall gap) - Stop with no samples - real Start → background sample → Stop that ends the sampler - `actionlint` is clean on both workflows. - This PR's own Windows CI run: results below. ### Observed in this PR's CI The data comes from three CI runs of this PR on Windows hosted runners: [41289502805](https://github.com/microsoft/aspire/actions/runs/41289502805), [41290605859](https://github.com/microsoft/aspire/actions/runs/41290605859) (both partly cancelled by later pushes) and [41291481771](https://github.com/microsoft/aspire/actions/runs/41291481771) (head `d0358d42`). It covers **331 Windows jobs with 2,923 samples**: 312 .NET test jobs and 19 VS Code extension E2E shards. The sampler emitted no missing-counter warnings. | | .NET test jobs (312) | Extension E2E shards (19) | |---|---|---| | CPU: mean of per-job averages | 54.8% | 44.3% | | CPU: share of samples at 90% or more | 27.6% | 6.5% | | Jobs with at least half their samples at 90% or more CPU | 44 | 0 | | Lowest available memory (of 16 GB) | 9,891 MB | 9,779 MB | | Peak commit charge (limit about 18.9 GB) | 33.4% | 36.6% | | Peak page file usage | 22% | 19.9% | | Peak disk queue length | 146.5 (one job; 4 jobs above 60) | 28.7 | | Lowest free space on C: | 25.6 GB | 23.5 GB | | Largest gap between samples | 37 s | 22 s | Examples: - [Extension E2E shard `discovery-configuration`](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124809523820): CPU 35% average (66% peak), at least 10.9 GB available, commit at most 26.7%, disk queue at most 11.6. The diagnostics artifact `extension-e2e-diagnostics-win-x64-discovery-configuration-attempt1` contains `resource-monitor/resources.csv` and `processes.log`. - [Hosting.Dotnet](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124805517960): CPU 84.5% average with 10 of 16 samples at 90% or more. The `logs-Hosting.Dotnet-windows-latest` artifact contains `resource-monitor/`. Its `processes.log` shows `VBCSCompiler`, `dotnet` and `MSBuild` as the top CPU consumers, i.e. the test-driven project builds. - [Cancelled Hosting-4](https://github.com/microsoft/aspire/actions/runs/41289502805/job/124798187412): Stop and upload still ran (CPU 82% average, disk queue at most 17.9). - The sampler's own `pwsh` uses about 1% CPU in `processes.log`. **Runner loss was reproduced in this run, and those jobs stayed dark.** On head `d0358d42`, three .NET jobs froze in `Run tests (Windows)` at about 09:15Z: [Hosting.Java](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124805518377) (2-minute baseline), [Hosting.Browsers](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124805521155) (3-minute baseline) and [Hosting-4](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124805533340) (8-minute baseline). MTP's `--timeout 20m` never fired. All three were declared lost about 46 minutes after job start: *"The hosted runner lost communication with the server"*, logs `BlobNotFound`, and no artifacts. The [extension E2E `winui-debug` shard](https://github.com/microsoft/aspire/actions/runs/41291481771/job/124809523893) (5–6-minute baseline) froze the same way in `Run extension E2E tests` from 09:34Z and was lost about 47 minutes after job start. This matches main: in 9 of the last 16 `ci.yml` push runs, 1–3 Windows jobs per run were lost at 46–48 minutes. In this PR's two earlier runs the same Java and Browsers jobs, also running the sampler, finished in 1–3 minutes. **What this answers so far:** 1. **Memory and commit:** not under pressure on any surviving Windows job. At least 9.8 GB was always available, commit charge stayed at or below 37%, and the page file at or below 22%. 2. **Disk:** space is fine (at least 23.5 GB free on C:, about 216 GB on D:). The disk queue spikes occasionally (above 60 in 4 of 331 jobs), during builds. 3. **CPU:** the 4-vCPU runners are often saturated during .NET test jobs, mostly by the builds that tests trigger. Extension E2E shards are moderate. No surviving job showed a stall: the largest gap between samples was 37s against a 20s interval. 4. **Frozen jobs:** they freeze within minutes of starting tests, and even the 2-minute Java suite froze. Telemetry can't see inside them, because a lost runner uploads nothing. So the data rules out gradual memory or disk exhaustion on the jobs we can observe, but can't confirm or exclude a sudden in-VM event. Capturing that would need an off-box sink, such as streaming samples to an external store. That is out of scope for this lightweight change. ## Checklist - Is this feature complete? - [x] Yes. Ready to ship. - [ ] No. Follow-up changes expected. - Are you including unit tests for the changes and scenario tests if relevant? - [x] Yes - [ ] No - Did you add public API? - [ ] Yes - [x] No - Does the change make any security assumptions or guarantees? - [ ] Yes - [x] No 🤖 Posted by AspireAgent (AI Agent) --------- Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 318bf52 commit 795f9be

5 files changed

Lines changed: 540 additions & 1 deletion

File tree

‎.github/workflows/extension-e2e-tests.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,15 @@ jobs:
555555
- name: Checkout code
556556
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
557557

558+
# Diagnostics only: samples CPU, memory, commit charge, page file, and disk pressure
559+
# into the diagnostics artifact so slow or hung Windows shards can be correlated with
560+
# machine load. https://github.com/microsoft/aspire/issues/21570
561+
- name: Start resource monitor (Windows)
562+
if: runner.os == 'Windows'
563+
continue-on-error: true
564+
shell: pwsh
565+
run: ./eng/scripts/ci-resource-monitor.ps1 -Mode Start -OutputDirectory "${{ github.workspace }}/extension/.test-results/resource-monitor"
566+
558567
- name: Setup Node.js
559568
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
560569
with:
@@ -1147,6 +1156,12 @@ jobs:
11471156
node scripts/run-e2e.js
11481157
}
11491158
1159+
- name: Stop resource monitor (Windows)
1160+
if: ${{ always() && runner.os == 'Windows' }}
1161+
continue-on-error: true
1162+
shell: pwsh
1163+
run: ./eng/scripts/ci-resource-monitor.ps1 -Mode Stop -OutputDirectory "${{ github.workspace }}/extension/.test-results/resource-monitor"
1164+
11501165
- name: Upload E2E diagnostics
11511166
id: upload_e2e_diagnostics
11521167
if: always()

‎.github/workflows/run-tests.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ jobs:
100100
- name: Checkout code
101101
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
102102

103+
# Diagnostics only: samples CPU, memory, commit charge, page file, and disk pressure
104+
# into the uploaded logs so slow or hung Windows jobs can be correlated with machine
105+
# load. https://github.com/microsoft/aspire/issues/21570
106+
- name: Start resource monitor (Windows)
107+
if: runner.os == 'Windows'
108+
continue-on-error: true
109+
shell: pwsh
110+
run: ./eng/scripts/ci-resource-monitor.ps1 -Mode Start -OutputDirectory "${{ github.workspace }}/artifacts/log/test-logs/resource-monitor"
111+
103112
- name: Set up .NET Core
104113
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
105114
# Used for running tests outside the repo
@@ -749,6 +758,12 @@ jobs:
749758
docker volume ls
750759
docker network ls
751760
761+
- name: Stop resource monitor (Windows)
762+
if: ${{ always() && runner.os == 'Windows' }}
763+
continue-on-error: true
764+
shell: pwsh
765+
run: ./eng/scripts/ci-resource-monitor.ps1 -Mode Stop -OutputDirectory "${{ github.workspace }}/artifacts/log/test-logs/resource-monitor"
766+
752767
- name: Upload logs, and test results
753768
id: upload-logs
754769
if: always()
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<#
2+
.SYNOPSIS
3+
Lightweight background resource sampler for Windows CI jobs.
4+
5+
.DESCRIPTION
6+
Diagnostics only: records machine pressure while a job runs so slow, hung, or
7+
lost Windows runners can be correlated with CPU, memory, commit, page file, and
8+
disk usage. See https://github.com/microsoft/aspire/issues/21570.
9+
10+
-Mode Start Launches a detached, below-normal-priority pwsh sampler and returns.
11+
-Mode Sample The sampler loop itself (started by -Mode Start).
12+
-Mode Stop Stops the sampler and prints a peak/minimum summary to the log and
13+
the job summary.
14+
15+
Output goes to -OutputDirectory, which callers place under a directory that an
16+
existing `if: always()` upload already collects:
17+
resources.csv One row per sample with system-wide counters.
18+
processes.log Top processes by CPU (since the previous sample) and by memory.
19+
sampler.log Sampler errors, if any.
20+
21+
Every mode always exits 0: telemetry must never change the job result.
22+
A runner that loses communication uploads nothing, so this cannot capture the
23+
last minutes of a lost runner. It covers slow, failed, cancelled, and passing jobs.
24+
#>
25+
param(
26+
[Parameter(Mandatory = $true)]
27+
[ValidateSet('Start', 'Sample', 'Stop')]
28+
[string]$Mode,
29+
30+
[Parameter(Mandatory = $true)]
31+
[string]$OutputDirectory,
32+
33+
[ValidateRange(5, 600)]
34+
[int]$IntervalSeconds = 20,
35+
36+
[ValidateRange(1, 50)]
37+
[int]$TopProcessCount = 5
38+
)
39+
40+
$ErrorActionPreference = 'Continue'
41+
$pidFile = Join-Path $OutputDirectory 'sampler.pid'
42+
$csvFile = Join-Path $OutputDirectory 'resources.csv'
43+
$processFile = Join-Path $OutputDirectory 'processes.log'
44+
$errorFile = Join-Path $OutputDirectory 'sampler.log'
45+
46+
$counterPaths = [ordered]@{
47+
CpuPercent = '\Processor(_Total)\% Processor Time'
48+
ProcessorQueue = '\System\Processor Queue Length'
49+
AvailableMB = '\Memory\Available MBytes'
50+
CommittedPercent = '\Memory\% Committed Bytes In Use'
51+
CommittedBytes = '\Memory\Committed Bytes'
52+
CommitLimitBytes = '\Memory\Commit Limit'
53+
PagesPerSec = '\Memory\Pages/sec'
54+
PageFilePercent = '\Paging File(_Total)\% Usage'
55+
DiskQueueLength = '\PhysicalDisk(_Total)\Avg. Disk Queue Length'
56+
DiskPercentTime = '\PhysicalDisk(_Total)\% Disk Time'
57+
}
58+
59+
$script:loggedCounterErrors = [System.Collections.Generic.HashSet[string]]::new()
60+
61+
function Write-SamplerError([string]$message) {
62+
try { Add-Content -LiteralPath $errorFile -Value "[$([DateTime]::UtcNow.ToString('o'))] $message" } catch { }
63+
}
64+
65+
function Invoke-Start {
66+
try {
67+
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
68+
$arguments = @(
69+
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
70+
'-File', "`"$PSCommandPath`"",
71+
'-Mode', 'Sample',
72+
'-OutputDirectory', "`"$OutputDirectory`"",
73+
'-IntervalSeconds', $IntervalSeconds,
74+
'-TopProcessCount', $TopProcessCount
75+
)
76+
$startArgs = @{ FilePath = 'pwsh'; ArgumentList = $arguments; PassThru = $true }
77+
if ($IsWindows) { $startArgs.WindowStyle = 'Hidden' }
78+
$process = Start-Process @startArgs
79+
try { $process.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::BelowNormal } catch { }
80+
Set-Content -LiteralPath $pidFile -Value $process.Id
81+
Write-Host "Resource monitor started (pid $($process.Id), every $IntervalSeconds s) -> $OutputDirectory"
82+
}
83+
catch {
84+
Write-Host "::warning::Resource monitor failed to start: $_"
85+
}
86+
}
87+
88+
function Get-CounterValues {
89+
$values = @{}
90+
try {
91+
# Counters that do not exist on this machine are skipped rather than failing the sample.
92+
$counterErrors = $null
93+
$samples = (Get-Counter -Counter @($counterPaths.Values) -ErrorAction SilentlyContinue -ErrorVariable counterErrors).CounterSamples
94+
foreach ($counterError in @($counterErrors | Where-Object { $_ })) {
95+
# Log each distinct failure once instead of on every sample.
96+
if ($script:loggedCounterErrors.Add("$counterError")) {
97+
Write-SamplerError "Get-Counter error: $counterError"
98+
}
99+
}
100+
foreach ($entry in $counterPaths.GetEnumerator()) {
101+
$leaf = $entry.Value.Split('\')[-1]
102+
$object = $entry.Value.Split('\')[1].Split('(')[0]
103+
$match = $samples | Where-Object { $_.Path -like "*\$object*\$leaf" } | Select-Object -First 1
104+
if ($match) { $values[$entry.Key] = $match.CookedValue }
105+
}
106+
}
107+
catch {
108+
if ($script:loggedCounterErrors.Add("$_")) {
109+
Write-SamplerError "Get-Counter failed: $_"
110+
}
111+
}
112+
return $values
113+
}
114+
115+
function Format-Number($value, [string]$format = 'F1') {
116+
if ($null -eq $value) { return '' }
117+
return ([double]$value).ToString($format, [System.Globalization.CultureInfo]::InvariantCulture)
118+
}
119+
120+
function Invoke-Sample {
121+
$logicalProcessors = [Environment]::ProcessorCount
122+
$columns = @('TimestampUtc', 'CpuPercent', 'ProcessorQueue', 'AvailableMB', 'CommittedPercent', 'CommittedGB', 'CommitLimitGB',
123+
'PagesPerSec', 'PageFilePercent', 'DiskQueueLength', 'DiskPercentTime', 'SystemDriveFreeGB', 'WorkspaceDriveFreeGB', 'ProcessCount')
124+
if (-not (Test-Path -LiteralPath $csvFile)) {
125+
Set-Content -LiteralPath $csvFile -Value ($columns -join ',')
126+
}
127+
128+
$systemRoot = if ($env:SystemDrive) { "$($env:SystemDrive)\" } else { [System.IO.Path]::GetPathRoot($PSHOME) }
129+
$workspaceRoot = if ($env:GITHUB_WORKSPACE) { [System.IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE) } else { $systemRoot }
130+
$previousCpu = @{}
131+
$previousTime = $null
132+
133+
while ($true) {
134+
$sampleStart = [DateTime]::UtcNow
135+
try {
136+
$counters = Get-CounterValues
137+
138+
$systemFree = $null
139+
$workspaceFree = $null
140+
try { $systemFree = [System.IO.DriveInfo]::new($systemRoot).AvailableFreeSpace / 1GB } catch { }
141+
try { $workspaceFree = [System.IO.DriveInfo]::new($workspaceRoot).AvailableFreeSpace / 1GB } catch { }
142+
143+
$processes = @(Get-Process -ErrorAction SilentlyContinue)
144+
$now = [DateTime]::UtcNow
145+
$currentCpu = @{}
146+
$cpuRows = foreach ($p in $processes) {
147+
$total = $null
148+
try { $total = $p.TotalProcessorTime.TotalSeconds } catch { }
149+
if ($null -eq $total) { continue }
150+
$key = "$($p.Id):$($p.ProcessName)"
151+
$currentCpu[$key] = $total
152+
if ($null -eq $previousTime) { continue }
153+
$delta = $null
154+
if ($previousCpu.ContainsKey($key)) {
155+
$delta = $total - $previousCpu[$key]
156+
}
157+
else {
158+
# A process that started during this interval spent all of its CPU time in it.
159+
try {
160+
if ($p.StartTime.ToUniversalTime() -ge $previousTime) { $delta = $total }
161+
}
162+
catch { }
163+
}
164+
$elapsed = ($now - $previousTime).TotalSeconds
165+
if ($null -ne $delta -and $elapsed -gt 0) {
166+
[pscustomobject]@{ Process = $p; Percent = 100 * $delta / $elapsed / $logicalProcessors }
167+
}
168+
}
169+
$previousCpu = $currentCpu
170+
$previousTime = $now
171+
172+
$row = @(
173+
$sampleStart.ToString('o'),
174+
(Format-Number $counters.CpuPercent),
175+
(Format-Number $counters.ProcessorQueue 'F0'),
176+
(Format-Number $counters.AvailableMB 'F0'),
177+
(Format-Number $counters.CommittedPercent),
178+
(Format-Number $(if ($null -ne $counters.CommittedBytes) { $counters.CommittedBytes / 1GB }) 'F2'),
179+
(Format-Number $(if ($null -ne $counters.CommitLimitBytes) { $counters.CommitLimitBytes / 1GB }) 'F2'),
180+
(Format-Number $counters.PagesPerSec 'F0'),
181+
(Format-Number $counters.PageFilePercent),
182+
(Format-Number $counters.DiskQueueLength 'F2'),
183+
(Format-Number $counters.DiskPercentTime),
184+
(Format-Number $systemFree 'F1'),
185+
(Format-Number $workspaceFree 'F1'),
186+
$processes.Count
187+
)
188+
Add-Content -LiteralPath $csvFile -Value ($row -join ',')
189+
190+
$topCpu = $cpuRows | Sort-Object Percent -Descending | Select-Object -First $TopProcessCount |
191+
ForEach-Object { "$($_.Process.ProcessName)[$($_.Process.Id)] $(Format-Number $_.Percent)%" }
192+
$topMemory = $processes | Sort-Object WorkingSet64 -Descending | Select-Object -First $TopProcessCount |
193+
ForEach-Object { "$($_.ProcessName)[$($_.Id)] ws=$(Format-Number ($_.WorkingSet64 / 1MB) 'F0')MB private=$(Format-Number ($_.PrivateMemorySize64 / 1MB) 'F0')MB" }
194+
Add-Content -LiteralPath $processFile -Value @(
195+
"[$($sampleStart.ToString('o'))]",
196+
" cpu: $($topCpu -join '; ')",
197+
" mem: $($topMemory -join '; ')"
198+
)
199+
}
200+
catch {
201+
Write-SamplerError "Sample failed: $_"
202+
}
203+
204+
$remaining = $IntervalSeconds - ([DateTime]::UtcNow - $sampleStart).TotalSeconds
205+
if ($remaining -gt 0) { Start-Sleep -Milliseconds ([int]($remaining * 1000)) }
206+
}
207+
}
208+
209+
function Get-Stat([object[]]$rows, [string]$column, [string]$kind) {
210+
$values = @($rows | ForEach-Object { $_.$column } | Where-Object { $_ -ne '' -and $null -ne $_ } |
211+
ForEach-Object { [double]::Parse($_, [System.Globalization.CultureInfo]::InvariantCulture) })
212+
if ($values.Count -eq 0) { return 'n/a' }
213+
$measure = $values | Measure-Object -Maximum -Minimum -Average
214+
switch ($kind) {
215+
'max' { return Format-Number $measure.Maximum }
216+
'min' { return Format-Number $measure.Minimum }
217+
'avg' { return Format-Number $measure.Average }
218+
}
219+
}
220+
221+
function Invoke-Stop {
222+
try {
223+
if (Test-Path -LiteralPath $pidFile) {
224+
$samplerPid = [int](Get-Content -LiteralPath $pidFile -Raw)
225+
try { [System.Diagnostics.Process]::GetProcessById($samplerPid).Kill($true) } catch { }
226+
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
227+
}
228+
229+
if (-not (Test-Path -LiteralPath $csvFile)) {
230+
Write-Host 'Resource monitor produced no samples.'
231+
return
232+
}
233+
234+
$rows = @(Import-Csv -LiteralPath $csvFile)
235+
if ($rows.Count -eq 0) {
236+
Write-Host 'Resource monitor produced no samples.'
237+
return
238+
}
239+
$maxGap = 0
240+
for ($i = 1; $i -lt $rows.Count; $i++) {
241+
$gap = ([DateTime]::Parse($rows[$i].TimestampUtc, $null, 'RoundtripKind') - [DateTime]::Parse($rows[$i - 1].TimestampUtc, $null, 'RoundtripKind')).TotalSeconds
242+
if ($gap -gt $maxGap) { $maxGap = $gap }
243+
}
244+
# Surface collection failures: an empty core counter column means Get-Counter is not working.
245+
$missingCounters = @('CpuPercent', 'AvailableMB', 'CommittedPercent', 'CommittedGB', 'CommitLimitGB', 'PageFilePercent', 'DiskQueueLength' | Where-Object {
246+
$column = $_
247+
-not ($rows | Where-Object { $_.$column -ne '' -and $null -ne $_.$column })
248+
})
249+
if ($missingCounters.Count -gt 0) {
250+
Write-Host "::warning::Resource monitor collected no values for: $($missingCounters -join ', '). See sampler.log in the artifact."
251+
}
252+
$highCpuSamples = @($rows | Where-Object { $_.CpuPercent -ne '' -and [double]::Parse($_.CpuPercent, [System.Globalization.CultureInfo]::InvariantCulture) -ge 90 }).Count
253+
254+
$summary = @(
255+
'### Windows runner resource monitor',
256+
'',
257+
"Samples: $($rows.Count), first $($rows[0].TimestampUtc), last $($rows[-1].TimestampUtc), largest gap between samples $(Format-Number $maxGap 'F0') s (a gap far above the interval means the machine stalled).",
258+
'',
259+
'| Metric | Average | Peak / minimum |',
260+
'|---|---|---|',
261+
"| CPU % | $(Get-Stat $rows CpuPercent avg) | max $(Get-Stat $rows CpuPercent max) ($highCpuSamples samples >= 90%) |",
262+
"| Processor queue length | $(Get-Stat $rows ProcessorQueue avg) | max $(Get-Stat $rows ProcessorQueue max) |",
263+
"| Available memory MB | $(Get-Stat $rows AvailableMB avg) | min $(Get-Stat $rows AvailableMB min) |",
264+
"| Commit charge % | $(Get-Stat $rows CommittedPercent avg) | max $(Get-Stat $rows CommittedPercent max) (limit $(Get-Stat $rows CommitLimitGB max) GB) |",
265+
"| Page file usage % | $(Get-Stat $rows PageFilePercent avg) | max $(Get-Stat $rows PageFilePercent max) |",
266+
"| Hard-fault paging pages/s | $(Get-Stat $rows PagesPerSec avg) | max $(Get-Stat $rows PagesPerSec max) |",
267+
"| Disk queue length | $(Get-Stat $rows DiskQueueLength avg) | max $(Get-Stat $rows DiskQueueLength max) |",
268+
"| System drive free GB | $(Get-Stat $rows SystemDriveFreeGB avg) | min $(Get-Stat $rows SystemDriveFreeGB min) |",
269+
"| Workspace drive free GB | $(Get-Stat $rows WorkspaceDriveFreeGB avg) | min $(Get-Stat $rows WorkspaceDriveFreeGB min) |",
270+
'',
271+
"Raw samples and top processes are in the job's diagnostics artifact under ``$(Split-Path -Leaf $OutputDirectory)``."
272+
)
273+
$summary | ForEach-Object { Write-Host $_ }
274+
if ($env:GITHUB_STEP_SUMMARY) {
275+
Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value $summary
276+
}
277+
}
278+
catch {
279+
Write-Host "::warning::Resource monitor summary failed: $_"
280+
}
281+
}
282+
283+
try {
284+
switch ($Mode) {
285+
'Start' { Invoke-Start }
286+
'Sample' { Invoke-Sample }
287+
'Stop' { Invoke-Stop }
288+
}
289+
}
290+
catch {
291+
Write-Host "::warning::Resource monitor $Mode failed: $_"
292+
}
293+
exit 0

‎extension/src/test/e2eLaunchProfile.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ suite('E2E launch profile', () => {
703703
const uploadStepIndex = workflow.indexOf('- name: Upload E2E diagnostics');
704704
const debuggerPrerequisiteStep = workflow.slice(debuggerPrerequisitesIndex, azureFunctionsPrerequisitesIndex);
705705
const azureFunctionsPrerequisiteStep = workflow.slice(azureFunctionsPrerequisitesIndex, workflow.indexOf('\n - name:', azureFunctionsPrerequisitesIndex));
706-
const runStep = workflow.slice(runStepIndex, uploadStepIndex);
706+
const runStep = workflow.slice(runStepIndex, workflow.indexOf('\n - name:', runStepIndex + 1));
707707
const uploadStep = workflow.slice(uploadStepIndex, workflow.indexOf('\n - name:', uploadStepIndex + 1));
708708
const csharpVersion = /csharp\/([^/]+)\/vspackage\?targetPlatform=linux-x64/.exec(debuggerPrerequisiteStep)?.[1];
709709
const minimumCsharpVersion = '2.145.15-prerelease';

0 commit comments

Comments
 (0)