From 8ea482c87ec9ea43072c9091a02ad32d1e58aec9 Mon Sep 17 00:00:00 2001 From: Robert Connell Date: Wed, 10 Dec 2025 21:36:59 -0500 Subject: [PATCH] Use dedicated thread when invoking buildSystem.build() as it is a blocking operation --- .../Basics/Concurrency/ConcurrencyHelpers.swift | 14 ++++++++++++++ Sources/Build/BuildOperation.swift | 14 ++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Sources/Basics/Concurrency/ConcurrencyHelpers.swift b/Sources/Basics/Concurrency/ConcurrencyHelpers.swift index 2837bea4b95..1007c598162 100644 --- a/Sources/Basics/Concurrency/ConcurrencyHelpers.swift +++ b/Sources/Basics/Concurrency/ConcurrencyHelpers.swift @@ -14,6 +14,7 @@ import _Concurrency import Dispatch import class Foundation.NSLock import class Foundation.ProcessInfo +import class Foundation.Thread import struct Foundation.URL import struct Foundation.UUID import func TSCBasic.tsc_await @@ -39,6 +40,19 @@ public func unsafe_await(_ body: @Sendable @escaping () async -> T) return box.get()! } +extension Task where Failure == Never { + /// Runs `block` in a new thread and suspends until it finishes execution. + /// + /// - note: This function should be used sparingly, such as for long-running operations that may block and therefore should not be run on the Swift Concurrency thread pool. Do not use this for operations for which there may be many concurrent invocations as it could lead to thread explosion. It is meant to be a bridge to pre-existing blocking code which can't easily be converted to use Swift concurrency features. + public static func detachNewThread(name: String? = nil, _ block: @Sendable @escaping () -> Success) async -> Success { + return await withCheckedContinuation { continuation in + Thread.detachNewThread { + Thread.current.name = name + return continuation.resume(returning: block()) + } + } + } +} extension DispatchQueue { // a shared concurrent queue for running concurrent asynchronous operations diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index ee3b48adfb6..831689605a4 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -291,7 +291,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS if self.cacheBuildManifest { do { // if buildPackageStructure returns a valid description we use that, otherwise we perform full planning - if try self.buildPackageStructure() { + if try await self.buildPackageStructure() { // confirm the step above created the build description as expected // we trust it to update the build description when needed let buildDescriptionPath = self.config.buildDescriptionPath(for: .target) @@ -829,15 +829,21 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Build the package structure target. - private func buildPackageStructure() throws -> Bool { + private func buildPackageStructure() async throws -> Bool { let (buildSystem, tracker) = try self.createBuildSystem( buildDescription: .none, config: self.config ) self.current = (buildSystem, tracker) - // Build the package structure target which will re-generate the llbuild manifest, if necessary. - let buildSuccess = buildSystem.build(target: "PackageStructure") + // We use Task.detachNewThread here because buildSystem.build() is a blocking + // operation. Running this on the Swift Concurrency thread pool can block a worker thread + // potentially causing thread pool starvation and deadlocks. By running it on a dedicated + // thread, we keep the Swift Concurrency pool available for other async work. + let buildSuccess = await _Concurrency.Task.detachNewThread(name: "buildPackageStructure") { + // Build the package structure target which will re-generate the llbuild manifest, if necessary. + buildSystem.build(target: "PackageStructure") + } // If progress has been printed this will add a newline to separate it from what could be // the output of the command. For instance `swift test --skip-build` may print progress for