Hive
Acceptance test runners are oversubscribed: 78 concurrent swift-frontend processes on a 6-core virtual machine
GitHub issue · Open
Why is this needed?
The acceptance test runner is oversubscribed by more than tenfold, which makes every acceptance test’s wall clock a function of what happens to be scheduled next to it rather than of the work it does. That is already producing recurring red builds, and it is getting worse over time.
The evidence comes from a spindump that Xcode captured automatically when BuildAcceptanceTestSwiftPMPrebuiltMacro blew its four minute time limit in run 79b1dfbb, snapshotted at 2026-08-20 12:56:14 UTC.
The machine:
Hardware model: VirtualMac2,1
Active cpus: 6
Memory size: 14 GB
Total CPU Time: 27.813s (of a possible 30s across 6 cores in the 5s sample = 93% saturated)
What was running on those 6 cores at that moment:
- 1,265 processes total
- 78 concurrent
swift-frontendprocesses - 51
git, 324bash, 21SWBBuildService, 9swift-package, 8ld
The fan-out that produces this is: one tuist test invocation starts three parallel xctest workers, each worker runs Swift Testing tests in parallel, and each of those tests shells out to its own nested xcodebuild, which starts its own build service and its own set of compiler processes. Confirmed by ancestry in the dump:
swift-frontend <- SWBBuildService <- xcodebuild <- xctest[50558] <- xcodebuild[48830] <- tuist[44592]
Split by worker, at the moment the failing test timed out:
| Worker | Descendant processes | swift-frontend |
CPU in the 5s window |
|---|---|---|---|
xctest[50556] |
18 | 16 | 2.42s |
xctest[50557] (the one that timed out) |
74 | 0 | 1.77s |
xctest[50558] |
60 | 37 | 10.65s |
The worker that failed was not doing heavy work. It had spawned its xcodebuild and was waiting on it, holding zero compiler processes of its own, while 53 compilers belonging to the other two workers consumed the machine. Its 16 seconds of actual work stretched past 240 seconds.
The effect is visible in aggregate too. Over nine days the suite became more concurrent, so the run finishes sooner while each individual test waits longer:
| Date | Tests | Avg test | Run wall clock | Effective concurrency |
|---|---|---|---|---|
| Aug 11 | 207 | 21.2s | 649s | 6.7x |
| Aug 14 | 208 | 23.9s | 565s | 8.8x |
| Aug 18 | 208 | 22.5s | 412s | 11.4x |
| Aug 20 | 207 | 29.2s | 368s | 16.4x |
Run wall clock is down 43% while per test wall clock is up around 38%. In that Aug 20 run, 20 tests took 180s or more and 6 took 240s or more. That is fine for tests with no ceiling and fatal for any test that has one, which is what #12511 is cleaning up. But removing ceilings only hides the symptom: the suite is still running roughly 78 compiler processes on 6 cores, which wastes wall clock on context switching and memory pressure rather than getting work done.
For calibration, the same test’s real work measured locally on an unloaded machine is about 16 seconds: tuist install 13.2s, tuist generate 2.6s, xcodebuild build 6.8s.
Worth ruling out explicitly, because it is the obvious first guess: this is not swift-syntax being compiled from source. The string swift-syntax does not appear anywhere in the spindump, and the timed out worker held no compiler processes at all. The prebuilt macro path is working and Swift Package Manager resolution is not the bottleneck.
Steps to address the need
The goal is to stop running dramatically more concurrent compiler processes than the machine has cores. Any one of these would help, and they can be combined:
-
Measure first. Add a step that records core count and load during an acceptance run, so the effect of any change below is visible rather than guessed. The spindump above is a one off; a cheap
sysctl -n hw.ncpuplus periodicuptimein the job would make this observable on every run. -
Cap the nested builds. Each acceptance test shells out to
xcodebuild, and each of those independently decides how many compiler jobs to run. Passing a job limit (for example-jobs) to thexcodebuildinvocations that acceptance tests spawn would bound the per test fan-out instead of letting each build assume it owns the machine. -
Cap test parallelism. The suite currently runs three
xctestworkers per machine, each running Swift Testing tests in parallel inside it. Reducing worker count, or bounding in process parallelism, trades a little throughput for far less contention. -
Reconsider the shard and machine shape. The suite runs with
--shard-total 2across 139 suites, so each machine takes roughly half. Either more shards across more machines, or larger machines than the current 6 core and 14 GB virtual machine, would give the existing parallelism room to actually run. -
While in here, check the Xcode version. The runner executed
/Applications/Xcode_26.6.app/...in this run even though.xcode-versionpins26.5. That may be intentional, but if the pin is meant to be authoritative then it is not being honoured, and acceptance tests are running on a different toolchain than the one we think we test against.
Related: #12511 removes the wall clock ceilings that this oversubscription was tripping. That change stops the false failures but does not reduce the oversubscription, which is why this is filed separately.
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
No GitHub comments yet.