ci: measure Windows Bazel test bottlenecks

This commit is contained in:
Adam Perry
2026-07-07 01:53:09 +00:00
parent 641aa1b619
commit ae95d8b0d9
2 changed files with 258 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
<#
Collect lightweight host telemetry around Windows Bazel CI runs.
The monitor mode intentionally uses built-in Windows performance counters so
the workflow does not need to install a profiler on the runner. The summarize
mode keeps the useful aggregate in the job log while the raw CSV remains
available as an artifact for deeper analysis.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('monitor', 'summarize')]
[string]$Mode,
[Parameter(Mandatory = $true)]
[string]$OutputPath,
[string]$StopFile,
[ValidateRange(1, 60)]
[int]$IntervalSeconds = 15
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-SingleCounterValue {
param(
[Parameter(Mandatory = $true)]
[object]$CounterSample,
[Parameter(Mandatory = $true)]
[string]$PathPattern
)
$sample = $CounterSample.CounterSamples |
Where-Object { $_.Path -like $PathPattern } |
Select-Object -First 1
if ($null -eq $sample) {
return 0
}
return [double]$sample.CookedValue
}
function Get-SummedCounterValue {
param(
[Parameter(Mandatory = $true)]
[object]$CounterSample,
[Parameter(Mandatory = $true)]
[string]$PathPattern
)
$samples = $CounterSample.CounterSamples |
Where-Object { $_.Path -like $PathPattern }
if ($null -eq $samples) {
return 0
}
return [double](($samples | Measure-Object -Property CookedValue -Sum).Sum)
}
function Get-Percentile {
param(
[Parameter(Mandatory = $true)]
[double[]]$Values,
[Parameter(Mandatory = $true)]
[ValidateRange(0, 100)]
[int]$Percentile
)
if ($Values.Count -eq 0) {
return 0
}
$sorted = $Values | Sort-Object
$index = [math]::Ceiling(($Percentile / 100) * $sorted.Count) - 1
return [double]$sorted[[math]::Max(0, $index)]
}
function Get-NumericColumn {
param(
[Parameter(Mandatory = $true)]
[object[]]$Rows,
[Parameter(Mandatory = $true)]
[string]$Name
)
return [double[]]@($Rows | ForEach-Object { [double]$_.$Name })
}
function Format-BytesPerSecond {
param([double]$Value)
return '{0:N1} MiB/s' -f ($Value / 1MB)
}
function Write-Summary {
if (-not (Test-Path -LiteralPath $OutputPath)) {
Write-Output "No Windows Bazel telemetry file found at $OutputPath."
return
}
$rows = @(Import-Csv -LiteralPath $OutputPath)
if ($rows.Count -eq 0) {
Write-Output "Windows Bazel telemetry file at $OutputPath had no samples."
return
}
$cpu = Get-NumericColumn -Rows $rows -Name cpu_percent
$processorQueue = Get-NumericColumn -Rows $rows -Name processor_queue_length
$diskRead = Get-NumericColumn -Rows $rows -Name disk_read_bytes_per_sec
$diskWrite = Get-NumericColumn -Rows $rows -Name disk_write_bytes_per_sec
$diskLatency = Get-NumericColumn -Rows $rows -Name disk_seconds_per_transfer
$diskQueue = Get-NumericColumn -Rows $rows -Name disk_queue_length
$networkReceived = Get-NumericColumn -Rows $rows -Name network_received_bytes_per_sec
$networkSent = Get-NumericColumn -Rows $rows -Name network_sent_bytes_per_sec
$memory = Get-NumericColumn -Rows $rows -Name available_memory_mib
$sampledSeconds = $rows.Count * $IntervalSeconds
$networkBytes = (($networkReceived | Measure-Object -Average).Average + ($networkSent | Measure-Object -Average).Average) * $sampledSeconds
$diskBytes = (($diskRead | Measure-Object -Average).Average + ($diskWrite | Measure-Object -Average).Average) * $sampledSeconds
Write-Output 'Windows Bazel host telemetry summary:'
Write-Output (' samples: {0} (~{1:N0}s)' -f $rows.Count, $sampledSeconds)
Write-Output (' cpu: avg {0:N1}%, p95 {1:N1}%, max {2:N1}%; processor queue max {3:N1}' -f
($cpu | Measure-Object -Average).Average,
(Get-Percentile -Values $cpu -Percentile 95),
($cpu | Measure-Object -Maximum).Maximum,
($processorQueue | Measure-Object -Maximum).Maximum)
Write-Output (' disk: read avg {0}, write avg {1}, p95 latency {2:N1}ms, queue max {3:N1}, sampled total {4:N2} GiB' -f
(Format-BytesPerSecond (($diskRead | Measure-Object -Average).Average)),
(Format-BytesPerSecond (($diskWrite | Measure-Object -Average).Average)),
((Get-Percentile -Values $diskLatency -Percentile 95) * 1000),
($diskQueue | Measure-Object -Maximum).Maximum,
($diskBytes / 1GB))
Write-Output (' network: receive avg {0}, send avg {1}, sampled total {2:N2} GiB' -f
(Format-BytesPerSecond (($networkReceived | Measure-Object -Average).Average)),
(Format-BytesPerSecond (($networkSent | Measure-Object -Average).Average)),
($networkBytes / 1GB))
Write-Output (' memory: minimum {0:N0} MiB available' -f ($memory | Measure-Object -Minimum).Minimum)
}
if ($Mode -eq 'summarize') {
Write-Summary
exit 0
}
if ([string]::IsNullOrWhiteSpace($StopFile)) {
throw 'monitor mode requires -StopFile.'
}
$outputDirectory = Split-Path -Parent $OutputPath
if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
}
$counterPaths = @(
'\Processor(_Total)\% Processor Time',
'\System\Processor Queue Length',
'\PhysicalDisk(_Total)\Disk Read Bytes/sec',
'\PhysicalDisk(_Total)\Disk Write Bytes/sec',
'\PhysicalDisk(_Total)\Avg. Disk sec/Transfer',
'\PhysicalDisk(_Total)\Avg. Disk Queue Length',
'\Network Interface(*)\Bytes Received/sec',
'\Network Interface(*)\Bytes Sent/sec',
'\Memory\Available MBytes'
)
while (-not (Test-Path -LiteralPath $StopFile)) {
try {
$sample = Get-Counter -Counter $counterPaths -ErrorAction Stop
$row = [pscustomobject]@{
timestamp = (Get-Date).ToUniversalTime().ToString('o')
cpu_percent = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\processor(_total)\% processor time'
processor_queue_length = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\system\processor queue length'
disk_read_bytes_per_sec = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\physicaldisk(_total)\disk read bytes/sec'
disk_write_bytes_per_sec = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\physicaldisk(_total)\disk write bytes/sec'
disk_seconds_per_transfer = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\physicaldisk(_total)\avg. disk sec/transfer'
disk_queue_length = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\physicaldisk(_total)\avg. disk queue length'
network_received_bytes_per_sec = Get-SummedCounterValue -CounterSample $sample -PathPattern '*\network interface(*)\bytes received/sec'
network_sent_bytes_per_sec = Get-SummedCounterValue -CounterSample $sample -PathPattern '*\network interface(*)\bytes sent/sec'
available_memory_mib = Get-SingleCounterValue -CounterSample $sample -PathPattern '*\memory\available mbytes'
}
$row | Export-Csv -LiteralPath $OutputPath -Append -NoTypeInformation
}
catch {
Write-Warning "Windows Bazel telemetry sample failed: $($_.Exception.Message)"
}
# Check the stop file once a second so the monitor does not add up to one
# full sampling interval to the measured workflow step during cleanup.
for ($second = 0; $second -lt $IntervalSeconds; $second += 1) {
if (Test-Path -LiteralPath $StopFile) {
break
}
Start-Sleep -Seconds 1
}
}
+58
View File
@@ -177,10 +177,44 @@ jobs:
BAZEL_TEST_SHARD: ${{ matrix.shard }}
BAZEL_TEST_SHARD_COUNT: 4
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
WINDOWS_BAZEL_ETL_PATH: ${{ runner.temp }}/windows-bazel-trace-shard-${{ matrix.shard }}.etl
WINDOWS_BAZEL_TELEMETRY_PATH: ${{ runner.temp }}/windows-bazel-telemetry-shard-${{ matrix.shard }}.csv
WINDOWS_BAZEL_TELEMETRY_STOP_FILE: ${{ runner.temp }}/windows-bazel-telemetry-stop-shard-${{ matrix.shard }}
shell: bash
run: |
set -euo pipefail
rm -f "${WINDOWS_BAZEL_ETL_PATH}" "${WINDOWS_BAZEL_TELEMETRY_PATH}" "${WINDOWS_BAZEL_TELEMETRY_STOP_FILE}"
pwsh -NoProfile -File .github/scripts/measure-windows-bazel.ps1 \
-Mode monitor \
-OutputPath "${WINDOWS_BAZEL_TELEMETRY_PATH}" \
-StopFile "${WINDOWS_BAZEL_TELEMETRY_STOP_FILE}" &
telemetry_pid=$!
etw_started=0
# Keep the trace broad enough to distinguish CPU, filesystem, disk,
# and network waits, but use light profiles to limit observer cost.
if wpr.exe \
-start CPU.light \
-start DiskIO.light \
-start FileIO.light \
-start Network.light \
-filemode; then
etw_started=1
else
echo "WPR resource profiles could not start; continuing with counter telemetry only." >&2
fi
stop_telemetry() {
touch "${WINDOWS_BAZEL_TELEMETRY_STOP_FILE}"
wait "${telemetry_pid}" || true
if [[ "${etw_started}" -eq 1 ]]; then
if ! wpr.exe -stop "${WINDOWS_BAZEL_ETL_PATH}" -skipPdbGen -compress; then
echo "WPR resource trace could not be saved; cancelling its session." >&2
wpr.exe -cancel || true
fi
fi
}
trap stop_telemetry EXIT
bazel_test_query='tests(//...) except tests(//third_party/v8:all) except attr(tags, "manual", tests(//...))'
mapfile -t bazel_targets < <(
./.github/scripts/run-bazel-query-ci.sh --output=label -- "${bazel_test_query}" \
@@ -211,8 +245,13 @@ jobs:
--skip_incompatible_explicit_targets
--test_tag_filters=-argument-comment-lint
--test_verbose_timeout_warnings
# Make every uploaded experiment revision execute its selected
# tests instead of reusing the prior revision's test action cache.
--test_env=CODEX_BAZEL_TEST_CACHE_BUST=${GITHUB_SHA}
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
--build_metadata=TAG_windows_test_shard=${BAZEL_TEST_SHARD}
--profile=${RUNNER_TEMP}/windows-bazel-profile-shard-${BAZEL_TEST_SHARD}.gz
--noslim_profile
)
./.github/scripts/run-bazel-ci.sh \
@@ -225,6 +264,25 @@ jobs:
-- \
"${selected_targets[@]}"
- name: Summarize Windows Bazel telemetry
if: always() && !cancelled()
continue-on-error: true
shell: pwsh
run: |
./.github/scripts/measure-windows-bazel.ps1 -Mode summarize -OutputPath "${{ runner.temp }}/windows-bazel-telemetry-shard-${{ matrix.shard }}.csv"
- name: Upload Windows Bazel telemetry
if: always() && !cancelled()
continue-on-error: true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: windows-bazel-telemetry-shard-${{ matrix.shard }}
path: |
${{ runner.temp }}/windows-bazel-telemetry-shard-${{ matrix.shard }}.csv
${{ runner.temp }}/windows-bazel-profile-shard-${{ matrix.shard }}.gz
${{ runner.temp }}/windows-bazel-trace-shard-${{ matrix.shard }}.etl
if-no-files-found: warn
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true