Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Sources/Basics/Concurrency/ConcurrencyHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,19 @@ public func unsafe_await<T: Sendable>(_ 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
Expand Down
14 changes: 10 additions & 4 deletions Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading