Skip to content

Commit b16bb8f

Browse files
authored
Testing: Run all test SPM executable actions in a operation queue (#9243)
Add an operation queue around execution of swiftPM process execution. **swift-testing** based test will run all tests in parallel at the same time, causing each invocation to spawn a process which will then spawn many other subprocesses. This is compounded by tests that use arguments, as each combination will create another test that will run in parallel. These leads to a process bomb consuming all CPU and causes massive I/O blocking. Add a limit to how many swiftPM top level processes can run at the same time during testing. This is similar behaviour to XCtest based testing with the **--num-workers** option limiting the number of occurrent tests.
1 parent 1f48ed4 commit b16bb8f

File tree

1 file changed

+31
-20
lines changed

1 file changed

+31
-20
lines changed

Sources/_InternalTestSupport/SwiftPMProduct.swift

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import struct Basics.AsyncProcessResult
1818

1919
import enum TSCBasic.ProcessEnv
2020

21+
// Fan out from invocation of SPM 'swift-*' commands can be quite large. Limit the number of concurrent tasks to a fraction of total CPUs.
22+
private let swiftPMExecutionQueue = AsyncOperationQueue(concurrentTasks: Int(Double(ProcessInfo.processInfo.activeProcessorCount) * 0.5))
23+
2124
/// Defines the executables used by SwiftPM.
2225
/// Contains path to the currently built executable and
2326
/// helper method to execute them.
@@ -82,28 +85,36 @@ extension SwiftPM {
8285
env: Environment? = nil,
8386
throwIfCommandFails: Bool = true
8487
) async throws -> (stdout: String, stderr: String) {
85-
let result = try await executeProcess(
86-
args,
87-
packagePath: packagePath,
88-
env: env
89-
)
90-
//Remove /r from stdout/stderr so that tests do not have to deal with them
91-
let stdout = try String(decoding: result.output.get().filter( { $0 != 13 }), as: Unicode.UTF8.self)
92-
let stderr = try String(decoding: result.stderrOutput.get().filter( { $0 != 13 }), as: Unicode.UTF8.self)
93-
94-
let returnValue = (stdout: stdout, stderr: stderr)
95-
if (!throwIfCommandFails) { return returnValue }
96-
97-
if result.exitStatus == .terminated(code: 0) {
98-
return returnValue
88+
// Swift Testing uses Swift concurrency for test execution and creates a task for each test to run in parallel.
89+
// A single invocation of "swift build" can spawn a large number of subprocesses.
90+
// When this pattern is repeated across many tests, thousands of processes compete for
91+
// CPU/disk/network resources. Tests can take thousands of seconds to complete, with periods
92+
// of no stdout/stderr output that can cause activity timeouts in CI pipelines.
93+
// Run all SPM executions under a queue to limit the maximum number of concurrent SPM processes.
94+
try await swiftPMExecutionQueue.withOperation {
95+
let result = try await executeProcess(
96+
args,
97+
packagePath: packagePath,
98+
env: env
99+
)
100+
// Remove /r from stdout/stderr so that tests do not have to deal with them
101+
let stdout = try String(decoding: result.output.get().filter { $0 != 13 }, as: Unicode.UTF8.self)
102+
let stderr = try String(decoding: result.stderrOutput.get().filter { $0 != 13 }, as: Unicode.UTF8.self)
103+
104+
let returnValue = (stdout: stdout, stderr: stderr)
105+
if !throwIfCommandFails { return returnValue }
106+
107+
if result.exitStatus == .terminated(code: 0) {
108+
return returnValue
109+
}
110+
throw SwiftPMError.executionFailure(
111+
underlying: AsyncProcessResult.Error.nonZeroExit(result),
112+
stdout: stdout,
113+
stderr: stderr
114+
)
99115
}
100-
throw SwiftPMError.executionFailure(
101-
underlying: AsyncProcessResult.Error.nonZeroExit(result),
102-
stdout: stdout,
103-
stderr: stderr
104-
)
105116
}
106-
117+
107118
private func executeProcess(
108119
_ args: [String],
109120
packagePath: AbsolutePath? = nil,

0 commit comments

Comments
 (0)