Enable Unity build for wslclib and enable CI unity builds (#41302)
Rename WSL_TEST_UNITY_BATCH_SIZE to WSL_UNITY_BATCH_SIZE and apply it to wslclib as well as wsltests, so both targets share one knob. The default is 0 (every file compiled separately) and the CI pipeline sets 4. Fixes the name collisions that surface when sources are merged into a single translation unit by fully qualifying details::WrapText, details::ArgConvertedTypeMapping, and ::IUnknown, hoisting the duplicated c_fallbackConsoleWidth constant into Terminal.h, and dropping redundant includes. Adds benchmarking scripts under tools/benchmarking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ggarzia-MSFT committed
Aug 12, 2026 at 12:02 UTC
91fe87dae7a3c424fc2327cf54f581287a899d70
15 files changed
+308
-26
.gitignore
+2
-1
@@ -78,4 +78,5 @@ deploy-log.txt
78
test-output*.txt
79
test-results.txt
80
testfile.txt
81
-output/
\ No newline at end of file
81
+output/
82
+tools/benchmarking/benchmark-results/
.pipelines/build-job.yml
+1
-1
@@ -124,7 +124,7 @@ jobs:
124
displayName: "CMake ${{ parameters.platform }}"
125
inputs:
126
workingDirectory: "."
127
- cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true -DWSL_INCLUDE_SDK_CSHARP=true $(packageInputDirArg)\${{ parameters.platform }}
127
+ cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true -DWSL_INCLUDE_SDK_CSHARP=true -DWSL_UNITY_BATCH_SIZE=4 $(packageInputDirArg)\${{ parameters.platform }}
128
129
# Workaround for WSL Settings NuGet restore authentication issue
130
- script: _deps\nuget.exe restore -NonInteractive
CMakeLists.txt
+7
-5
@@ -188,14 +188,16 @@ if (NOT DEFINED WSL_INCLUDE_SDK_CSHARP)
188
set(WSL_INCLUDE_SDK_CSHARP false)
189
endif ()
190
191
-# Number of test sources combined into each unity translation unit.
191
+# Number of sources combined into each unity translation unit.
192
# Set to 0 to compile every file separately.
193
-if (NOT DEFINED WSL_TEST_UNITY_BATCH_SIZE)
194
- set(WSL_TEST_UNITY_BATCH_SIZE 2)
193
+set(WSL_UNITY_BATCH_SIZE_DEFAULT 0)
194
+
195
+if (NOT DEFINED WSL_UNITY_BATCH_SIZE)
196
+ set(WSL_UNITY_BATCH_SIZE ${WSL_UNITY_BATCH_SIZE_DEFAULT})
197
endif ()
198
197
-if (NOT WSL_TEST_UNITY_BATCH_SIZE MATCHES "^[0-9]+$")
198
- message(FATAL_ERROR "WSL_TEST_UNITY_BATCH_SIZE must be a non-negative integer: got '${WSL_TEST_UNITY_BATCH_SIZE}'")
199
+if (NOT WSL_UNITY_BATCH_SIZE MATCHES "^[0-9]+$")
200
+ message(FATAL_ERROR "WSL_UNITY_BATCH_SIZE must be a non-negative integer: got '${WSL_UNITY_BATCH_SIZE}'")
201
endif ()
202
find_commit_hash(COMMIT_HASH)
203
UserConfig.cmake.sample
+3
-3
@@ -65,6 +65,6 @@ endif()
65
# # fix - automatically fix formatting and re-stage files
66
# set(WSL_PRE_COMMIT_MODE "warn")
67
68
-# # Uncomment to change how many test sources share a unity translation unit (default: 2).
69
-# # Use 0 to disable unity builds and compile every test file separately.
70
-# set(WSL_TEST_UNITY_BATCH_SIZE 4)
68
+# # Uncomment to change how many sources share a unity translation unit (default: 0).
69
+# # Use 0 to disable unity builds and compile every file separately.
70
+# set(WSL_UNITY_BATCH_SIZE 4)
src/windows/wslc/CMakeLists.txt
+8
@@ -9,6 +9,14 @@ file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${SOURCE_PATTERNS})
9
# Object library for WSLC components.
10
# Used to build the executable and also unit testing components.
11
add_library(wslclib OBJECT ${SOURCES} ${HEADERS})
12
+
13
+if (DEFINED WSL_UNITY_BATCH_SIZE AND WSL_UNITY_BATCH_SIZE GREATER 0)
14
+ set_target_properties(wslclib PROPERTIES
15
+ UNITY_BUILD ON
16
+ UNITY_BUILD_MODE BATCH
17
+ UNITY_BUILD_BATCH_SIZE ${WSL_UNITY_BATCH_SIZE})
18
+endif ()
19
+
20
target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUBDIR_PATHS})
21
22
target_link_libraries(wslclib
src/windows/wslc/arguments/ArgumentValidation.cpp
+1
-1
@@ -51,7 +51,7 @@ namespace {
51
template <ArgType A, typename Converter>
52
void CacheConverted(ArgMap& execArgs, const std::wstring& argName, Converter&& convert)
53
{
54
- using value_t = typename details::ArgConvertedTypeMapping<A>::value_t;
54
+ using value_t = typename wsl::windows::wslc::argument::details::ArgConvertedTypeMapping<A>::value_t;
55
using converted_t = decltype(convert(std::declval<const std::wstring&>(), std::declval<const std::wstring&>()));
56
static_assert(
57
std::is_same_v<converted_t, value_t>,
src/windows/wslc/core/TableOutput.h
+2
-2
@@ -385,7 +385,7 @@ private:
385
// For plain cells, wrap the text directly.
386
if (cell.sequences.empty())
387
{
388
- auto chunks = details::WrapText(cell.fmt, col.MaxLength);
388
+ auto chunks = wsl::windows::wslc::details::WrapText(cell.fmt, col.MaxLength);
389
std::vector<FormattedCell> result;
390
result.reserve(chunks.size());
391
for (auto& chunk : chunks)
@@ -416,7 +416,7 @@ private:
416
}
417
}
418
419
- auto chunks = details::WrapText(visibleText, col.MaxLength);
419
+ auto chunks = wsl::windows::wslc::details::WrapText(visibleText, col.MaxLength);
420
std::vector<FormattedCell> result;
421
result.reserve(chunks.size());
422
for (auto& chunk : chunks)
src/windows/wslc/core/Terminal.h
+5
@@ -31,6 +31,11 @@ Abstract:
31
32
namespace wsl::windows::wslc {
33
34
+// Fallback width for progress displays when the console width can't be queried. This
35
+// value already includes the autowrap guard (visible width minus one) so a wrapped line
36
+// can't corrupt cursor-based rendering.
37
+inline constexpr int c_fallbackConsoleWidth = 79;
38
+
39
namespace terminal_detail {
40
41
// SFINAE: excludes Sequence-derived types so the overload below wins for them.
src/windows/wslc/services/BuildImageCallback.cpp
-3
@@ -20,9 +20,6 @@ namespace wsl::windows::wslc::services {
20
using wsl::windows::common::string::MultiByteToWide;
21
using namespace wsl::windows::common::vt;
22
23
-// Fallback width used when the console width can't be queried.
24
-constexpr int c_fallbackConsoleWidth = 79;
25
-
23
BuildImageCallback::~BuildImageCallback()
24
try
25
{
src/windows/wslc/services/ImageProgressCallback.cpp
-5
@@ -21,11 +21,6 @@ namespace wsl::windows::wslc::services {
21
using namespace wsl::shared;
22
using namespace wsl::windows::common::vt;
23
24
-// Fallback width for the in-place progress display when the console width can't be queried. This
25
-// value already includes the autowrap guard (visible width minus one) so a wrapped line can't
26
-// corrupt the cursor-based rendering.
27
-constexpr int c_fallbackConsoleWidth = 79;
28
-
24
auto ImageProgressCallback::MoveToLine(int line)
25
{
26
if (line > 0)
src/windows/wslc/services/SessionModel.h
+2
-2
@@ -34,9 +34,9 @@ struct Session
34
// Acquires an activity token that keeps the VM alive for the duration of a client-side
35
// container operation (resolve + operate, plus any streamed output). Hold the returned
36
// pointer for the whole operation; releasing it lets the VM idle-terminate again.
37
- [[nodiscard]] wil::com_ptr<IUnknown> BeginContainerOperation() const
37
+ [[nodiscard]] wil::com_ptr<::IUnknown> BeginContainerOperation() const
38
{
39
- wil::com_ptr<IUnknown> operation;
39
+ wil::com_ptr<::IUnknown> operation;
40
THROW_IF_FAILED(m_session->BeginContainerOperation(&operation));
41
return operation;
42
}
src/windows/wslc/tasks/SessionTasks.cpp
-1
@@ -20,7 +20,6 @@ Abstract:
20
#include "Task.h"
21
22
using namespace wsl::shared;
23
-using namespace wsl::shared::string;
23
using namespace wsl::windows::common::string;
24
using namespace wsl::windows::common::wslutil;
25
using namespace wsl::windows::wslc::execution;
test/windows/CMakeLists.txt
+2
-2
@@ -24,11 +24,11 @@ add_compile_definitions(INLINE_TEST_METHOD_MARKUP)
24
25
add_library(wsltests SHARED ${SOURCES} ${HEADERS})
26
27
-if (DEFINED WSL_TEST_UNITY_BATCH_SIZE AND WSL_TEST_UNITY_BATCH_SIZE GREATER 0)
27
+if (DEFINED WSL_UNITY_BATCH_SIZE AND WSL_UNITY_BATCH_SIZE GREATER 0)
28
set_target_properties(wsltests PROPERTIES
29
UNITY_BUILD ON
30
UNITY_BUILD_MODE BATCH
31
- UNITY_BUILD_BATCH_SIZE ${WSL_TEST_UNITY_BATCH_SIZE})
31
+ UNITY_BUILD_BATCH_SIZE ${WSL_UNITY_BATCH_SIZE})
32
endif ()
33
34
target_include_directories(wsltests PRIVATE
tools/benchmarking/benchmark-build.ps1
new
+208
@@ -0,0 +1,208 @@
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
+}
tools/benchmarking/run-unity-batch-sweep.ps1
new
+67
@@ -0,0 +1,67 @@
1
+<#
2
+.SYNOPSIS
3
+ Runs benchmark-build.ps1 across several unity-build batch sizes.
4
+
5
+.DESCRIPTION
6
+ For each batch size the project is reconfigured, stale unity sources are removed,
7
+ and a full benchmark run is performed. Labels are "<Prefix>-<name>", where a batch
8
+ size of 0 is labelled "nounity".
9
+
10
+.EXAMPLE
11
+ powershell tools\benchmarking\run-unity-batch-sweep.ps1 -Prefix split-before
12
+
13
+.EXAMPLE
14
+ powershell tools\benchmarking\run-unity-batch-sweep.ps1 -Prefix wslc -SourceDir test\windows\wslc
15
+#>
16
+[CmdletBinding()]
17
+param(
18
+ [Parameter(Mandatory = $true)][string]$Prefix,
19
+ [int[]]$BatchSizes = @(0, 2, 4, 8),
20
+ [int]$FullBuilds = 10,
21
+ [int]$IncrementalBuilds = 100,
22
+ [string]$Config = 'debug',
23
+ [string]$Target = 'wsltests',
24
+ [string]$TargetDir = 'test\windows',
25
+ [string]$SourceDir,
26
+ [string]$UnityVariable = 'WSL_UNITY_BATCH_SIZE'
27
+)
28
+
29
+$ErrorActionPreference = 'Stop'
30
+
31
+$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
32
+Push-Location $repoRoot
33
+try
34
+{
35
+ if (-not $SourceDir) { $SourceDir = $TargetDir }
36
+
37
+ foreach ($batch in $BatchSizes)
38
+ {
39
+ $name = if ($batch -eq 0) { 'nounity' } else { "batch$batch" }
40
+ $label = "$Prefix-$name"
41
+
42
+ Write-Host "`n########## $label ($UnityVariable=$batch) ##########" -ForegroundColor Magenta
43
+
44
+ & cmake . "-D$UnityVariable=$batch" | Out-Null
45
+ if ($LASTEXITCODE -ne 0) { throw "cmake configure failed for batch size $batch." }
46
+
47
+ # Generated unity sources are not pruned when the batch size changes, and the batch
48
+ # size is shared by every unity-enabled target, so clear all of them.
49
+ Get-ChildItem $repoRoot -Directory -Recurse -Filter Unity |
50
+ Where-Object { $_.Parent.Name -like '*.dir' } |
51
+ ForEach-Object { Remove-Item $_.FullName -Recurse -Force }
52
+
53
+ & cmake . "-D$UnityVariable=$batch" | Out-Null
54
+ if ($LASTEXITCODE -ne 0) { throw "cmake reconfigure failed for batch size $batch." }
55
+
56
+ & powershell -NoProfile -File (Join-Path $PSScriptRoot 'benchmark-build.ps1') `
57
+ -Label $label -FullBuilds $FullBuilds -IncrementalBuilds $IncrementalBuilds `
58
+ -Config $Config -Target $Target -TargetDir $TargetDir -SourceDir $SourceDir
59
+ if ($LASTEXITCODE -ne 0) { throw "Benchmark failed for $label." }
60
+ }
61
+
62
+ Write-Host "`n########## SWEEP COMPLETE ($Prefix) ##########" -ForegroundColor Green
63
+}
64
+finally
65
+{
66
+ Pop-Location
67
+}