master
ps1 208 lines 7.4 KB
Raw
1 <#
2 .SYNOPSIS
3 Benchmarks full and incremental build times for a CMake target.
4
5 .DESCRIPTION
6 Runs N full builds (object directory wiped before each) and M incremental builds
7 (a randomly chosen .cpp under -SourceDir gets a comment appended, then the target
8 is rebuilt and the file is restored byte-for-byte).
9
10 After each incremental iteration the restored file is rebuilt in an untimed settle
11 build, so a measurement only ever covers the file touched by that iteration.
12
13 Results are written to a CSV and a summary is printed.
14
15 .PARAMETER Target
16 CMake target to build. Defaults to 'wsltests'.
17
18 .PARAMETER TargetDir
19 Directory (relative to the repo root, or absolute) holding the CMakeLists.txt that
20 defines -Target. Used to locate the target's object directory. Defaults to
21 'test\windows'.
22
23 .PARAMETER SourceDir
24 Directory whose .cpp files are touched during incremental builds. Defaults to
25 -TargetDir.
26
27 .PARAMETER ExcludeDirName
28 Directory names skipped when collecting incremental candidates.
29
30 .EXAMPLE
31 powershell tools\benchmarking\benchmark-build.ps1 -Label before -FullBuilds 10 -IncrementalBuilds 100
32
33 .EXAMPLE
34 powershell tools\benchmarking\benchmark-build.ps1 -Label wslc-only -SourceDir test\windows\wslc
35
36 .EXAMPLE
37 powershell tools\benchmarking\benchmark-build.ps1 -Label svc -Target wslservice -TargetDir src\windows\service\exe
38 #>
39 [CmdletBinding()]
40 param(
41 [Parameter(Mandatory = $true)][string]$Label,
42 [int]$FullBuilds = 10,
43 [int]$IncrementalBuilds = 100,
44 [string]$Config = 'debug',
45 [string]$Target = 'wsltests',
46 [string]$TargetDir = 'test\windows',
47 [string]$SourceDir,
48 [string[]]$ExcludeDirName = @('testplugin'),
49 [string]$OutDir = 'tools\benchmarking\benchmark-results',
50 [int]$Seed = 20260803
51 )
52
53 $ErrorActionPreference = 'Stop'
54
55 $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
56 Push-Location $repoRoot
57 try
58 {
59 function Resolve-RepoPath
60 {
61 param([string]$Path)
62
63 if ([System.IO.Path]::IsPathRooted($Path)) { return $Path }
64 return (Join-Path $repoRoot $Path)
65 }
66
67 if (-not $SourceDir) { $SourceDir = $TargetDir }
68
69 $targetDirFull = Resolve-RepoPath $TargetDir
70 $sourceDirFull = Resolve-RepoPath $SourceDir
71 $objDir = Join-Path $targetDirFull "$Target.dir\$Config"
72
73 if (-not (Test-Path (Join-Path $repoRoot 'CMakeCache.txt')))
74 {
75 throw "CMakeCache.txt not found in $repoRoot. Run 'cmake .' first."
76 }
77
78 if (-not (Test-Path $targetDirFull))
79 {
80 throw "TargetDir not found: $targetDirFull"
81 }
82
83 if (-not (Test-Path $sourceDirFull))
84 {
85 throw "SourceDir not found: $sourceDirFull"
86 }
87
88 if (-not (Test-Path $OutDir))
89 {
90 New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
91 }
92
93 $csvPath = Join-Path $OutDir "$Label.csv"
94 $logPath = Join-Path $OutDir "$Label.log"
95 Remove-Item $csvPath, $logPath -ErrorAction SilentlyContinue
96
97 $script:results = New-Object System.Collections.Generic.List[object]
98
99 function Invoke-TimedBuild
100 {
101 param([string]$Phase, [int]$Iteration, [int]$Total, [string]$TouchedFile)
102
103 $sw = [Diagnostics.Stopwatch]::StartNew()
104 $output = & cmake --build . --target $Target --config $Config -- -m 2>&1
105 $sw.Stop()
106 $exit = $LASTEXITCODE
107
108 Add-Content -Path $logPath -Value "===== $Phase #$Iteration ($TouchedFile) exit=$exit elapsed=$($sw.Elapsed.TotalSeconds) ====="
109 Add-Content -Path $logPath -Value ($output | Out-String)
110
111 if ($exit -ne 0)
112 {
113 throw "Build failed during $Phase iteration $Iteration. See $logPath"
114 }
115
116 $record = [pscustomobject]@{
117 Label = $Label
118 Phase = $Phase
119 Iteration = $Iteration
120 File = $TouchedFile
121 Seconds = [math]::Round($sw.Elapsed.TotalSeconds, 3)
122 }
123 $script:results.Add($record)
124 $record | Export-Csv -Path $csvPath -NoTypeInformation -Append
125 Write-Host ("[{0}] {1,-11} {2,3}/{3,-3} {4,7:N2}s {5}" -f $Label, $Phase, $Iteration, $Total, $record.Seconds, $TouchedFile)
126 }
127
128 Write-Host "Warm-up build (bring all dependencies up to date)..." -ForegroundColor Cyan
129
130 function Reset-DebugDatabase
131 {
132 # Incremental links keep appending to the program database, which eventually
133 # trips LNK1140 (4 GB limit) part way through a long run.
134 Get-ChildItem (Join-Path $repoRoot 'bin') -Recurse -Include "$Target.pdb", "$Target.ilk" -ErrorAction SilentlyContinue |
135 Remove-Item -Force -ErrorAction SilentlyContinue
136 }
137
138 Reset-DebugDatabase
139 & cmake --build . --target $Target --config $Config -- -m | Out-Null
140 if ($LASTEXITCODE -ne 0) { throw 'Warm-up build failed.' }
141
142 Write-Host "`n=== FULL BUILDS ($FullBuilds) ===" -ForegroundColor Cyan
143 for ($i = 1; $i -le $FullBuilds; $i++)
144 {
145 if (Test-Path $objDir) { Remove-Item $objDir -Recurse -Force }
146 Reset-DebugDatabase
147 Invoke-TimedBuild -Phase 'full' -Iteration $i -Total $FullBuilds -TouchedFile '(clean)'
148 }
149
150 Write-Host "`n=== INCREMENTAL BUILDS ($IncrementalBuilds) ===" -ForegroundColor Cyan
151 $candidates = @(
152 Get-ChildItem -Path $sourceDirFull -Filter *.cpp -Recurse |
153 Where-Object { (Split-Path $_.DirectoryName -Leaf) -notin $ExcludeDirName } |
154 Select-Object -ExpandProperty FullName
155 )
156 if ($candidates.Count -eq 0) { throw "No .cpp files found under $sourceDirFull" }
157 Write-Host "Incremental candidate pool: $($candidates.Count) file(s) from $sourceDirFull"
158
159 # Touches accumulate for the duration of the run and every file is restored at the
160 # end. Restoring in the loop would re-dirty the file and make the next iteration
161 # rebuild this iteration's translation unit as well as its own.
162 $rand = New-Object System.Random($Seed)
163 $originals = @{}
164 try
165 {
166 for ($i = 1; $i -le $IncrementalBuilds; $i++)
167 {
168 $file = $candidates[$rand.Next(0, $candidates.Count)]
169 if (-not $originals.ContainsKey($file))
170 {
171 $originals[$file] = [System.IO.File]::ReadAllBytes($file)
172 }
173
174 Add-Content -Path $file -Value "`n// build-benchmark touch $Label $i"
175 Invoke-TimedBuild -Phase 'incremental' -Iteration $i -Total $IncrementalBuilds -TouchedFile (Split-Path $file -Leaf)
176 }
177 }
178 finally
179 {
180 foreach ($entry in $originals.GetEnumerator())
181 {
182 [System.IO.File]::WriteAllBytes($entry.Key, $entry.Value)
183 }
184 Write-Host "Restored $($originals.Count) touched file(s)."
185 }
186
187 Write-Host "`n=== SUMMARY ($Label) ===" -ForegroundColor Green
188 $script:results | Group-Object Phase | ForEach-Object {
189 $s = $_.Group.Seconds | Measure-Object -Average -Minimum -Maximum -Sum
190 $sorted = @($_.Group.Seconds | Sort-Object)
191 $median = if ($sorted.Count % 2) { $sorted[[int]($sorted.Count / 2)] } else { ($sorted[$sorted.Count / 2 - 1] + $sorted[$sorted.Count / 2]) / 2 }
192 [pscustomobject]@{
193 Phase = $_.Name
194 Count = $s.Count
195 MeanSec = [math]::Round($s.Average, 2)
196 MedSec = [math]::Round($median, 2)
197 MinSec = [math]::Round($s.Minimum, 2)
198 MaxSec = [math]::Round($s.Maximum, 2)
199 TotSec = [math]::Round($s.Sum, 2)
200 }
201 } | Format-Table -AutoSize
202
203 Write-Host "Results: $csvPath"
204 }
205 finally
206 {
207 Pop-Location
208 }