Skip to content

Commit 8017926

Browse files
authored
[6.2] Testing: Run all test SPM executable actions in a operation queue (#9243) (#9266)
1 parent 93e22ec commit 8017926

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)