@@ -18,6 +18,9 @@ import struct Basics.AsyncProcessResult
1818
1919import 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