Skip to content
Open
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
2 changes: 1 addition & 1 deletion Sources/Basics/Cancellator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public final class Cancellator: Cancellable, Sendable {

@discardableResult
public func register(name: String, handler: @escaping CancellationHandler) -> RegistrationKey? {
if self.cancelling.get(default: false) {
if self.cancelling.get() {
self.observabilityScope?.emit(debug: "not registering '\(name)' with terminator, termination in progress")
return .none
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Basics/Concurrency/AsyncProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ package final class AsyncProcess {
package func waitUntilExit() throws -> AsyncProcessResult {
let group = DispatchGroup()
group.enter()
let resultBox = ThreadSafeBox<Result<AsyncProcessResult, Swift.Error>>()
let resultBox = ThreadSafeBox<Result<AsyncProcessResult, Swift.Error>?>()
self.waitUntilExit { result in
resultBox.put(result)
group.leave()
Expand Down
2 changes: 1 addition & 1 deletion Sources/Basics/Concurrency/ConcurrencyHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public enum Concurrency {
public func unsafe_await<T: Sendable>(_ body: @Sendable @escaping () async -> T) -> T {
let semaphore = DispatchSemaphore(value: 0)

let box = ThreadSafeBox<T>()
let box = ThreadSafeBox<T?>()
Task {
let localValue: T = await body()
box.mutate { _ in localValue }
Expand Down
194 changes: 135 additions & 59 deletions Sources/Basics/Concurrency/ThreadSafeBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,135 +12,211 @@

import class Foundation.NSLock

/// Thread-safe value boxing structure
/// Thread-safe value boxing structure that provides synchronized access to a wrapped value.
@dynamicMemberLookup
public final class ThreadSafeBox<Value> {
private var underlying: Value?
private var underlying: Value
private let lock = NSLock()

public init() {}

/// Creates a new thread-safe box with the given initial value.
///
/// - Parameter seed: The initial value to store in the box.
public init(_ seed: Value) {
self.underlying = seed
}

public func mutate(body: (Value?) throws -> Value?) rethrows {
/// Atomically mutates the stored value by applying a transformation function.
///
/// The transformation function receives the current value and returns a new value
/// to replace it. The entire operation is performed under a lock to ensure atomicity.
///
/// - Parameter body: A closure that takes the current value and returns a new value.
/// - Throws: Any error thrown by the transformation function.
public func mutate(body: (Value) throws -> Value) rethrows {
try self.lock.withLock {
let value = try body(self.underlying)
self.underlying = value
}
}

public func mutate(body: (inout Value?) throws -> ()) rethrows {

/// Atomically mutates the stored value by applying an in-place transformation.
///
/// The transformation function receives an inout reference to the current value,
/// allowing direct modification. The entire operation is performed under a lock
/// to ensure atomicity.
///
/// - Parameter body: A closure that receives an inout reference to the current value.
/// - Throws: Any error thrown by the transformation function.
public func mutate(body: (inout Value) throws -> Void) rethrows {
try self.lock.withLock {
try body(&self.underlying)
}
}

@discardableResult
public func memoize(body: () throws -> Value) rethrows -> Value {
if let value = self.get() {
return value
}
let value = try body()
/// Atomically retrieves the current value from the box.
///
/// - Returns: A copy of the current value stored in the box.
public func get() -> Value {
self.lock.withLock {
self.underlying = value
self.underlying
}
return value
}

@discardableResult
public func memoize(body: () async throws -> Value) async rethrows -> Value {
if let value = self.get() {
return value
}
let value = try await body()
/// Atomically replaces the current value with a new value.
///
/// - Parameter newValue: The new value to store in the box.
public func put(_ newValue: Value) {
self.lock.withLock {
self.underlying = value
self.underlying = newValue
}
return value
}

public func clear() {
/// Provides thread-safe read-only access to properties of the wrapped value.
///
/// This subscript allows you to access properties of the wrapped value using
/// dot notation while maintaining thread safety.
///
/// - Parameter keyPath: A key path to a property of the wrapped value.
/// - Returns: The value of the specified property.
public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
self.lock.withLock {
self.underlying = nil
self.underlying[keyPath: keyPath]
}
}

public func get() -> Value? {
self.lock.withLock {
self.underlying
/// Provides thread-safe read-write access to properties of the wrapped value.
///
/// - Parameter keyPath: A writable key path to a property of the wrapped value.
/// - Returns: The value of the specified property when getting.
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get {
self.lock.withLock {
self.underlying[keyPath: keyPath]
}
}
set {
self.lock.withLock {
self.underlying[keyPath: keyPath] = newValue
}
}
}
}

public func get(default: Value) -> Value {
self.lock.withLock {
self.underlying ?? `default`
}
// Extension for optional values to support empty initialization
extension ThreadSafeBox {
/// Creates a new thread-safe box initialized with nil for optional value types.
///
/// This convenience initializer is only available when the wrapped value type is optional.
public convenience init<Wrapped>() where Value == Wrapped? {
self.init(nil)
}

public func put(_ newValue: Value) {
/// Takes the stored optional value, setting it to nil.
/// - Returns: The previously stored value, or nil if none was present.
public func takeValue<Wrapped>() -> Value where Value == Wrapped? {
self.lock.withLock {
self.underlying = newValue
guard let value = self.underlying else { return nil }
self.underlying = nil
return value
}
}

public func takeValue<U>() -> Value where U? == Value {
/// Atomically sets the stored optional value to nil.
///
/// This method is only available when the wrapped value type is optional.
public func clear<Wrapped>() where Value == Wrapped? {
self.lock.withLock {
guard let value = self.underlying else { return nil }
self.underlying = nil
return value
}
}

public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T? {
/// Atomically retrieves the stored value, returning a default if nil.
///
/// This method is only available when the wrapped value type is optional.
///
/// - Parameter defaultValue: The value to return if the stored value is nil.
/// - Returns: The stored value if not nil, otherwise the default value.
public func get<Wrapped>(default defaultValue: Wrapped) -> Wrapped where Value == Wrapped? {
self.lock.withLock {
self.underlying?[keyPath: keyPath]
self.underlying ?? defaultValue
}
}

public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T?>) -> T? {
get {
self.lock.withLock {
self.underlying?[keyPath: keyPath]
/// Atomically computes and caches a value if not already present.
///
/// If the box already contains a non-nil value, that value is returned immediately.
/// Otherwise, the provided closure is executed to compute the value, which is then
/// stored and returned. This method is only available when the wrapped value type is optional.
///
/// - Parameter body: A closure that computes the value to store if none exists.
/// - Returns: The cached value or the newly computed value.
/// - Throws: Any error thrown by the computation closure.
@discardableResult
public func memoize<Wrapped>(body: () throws -> Wrapped) rethrows -> Wrapped where Value == Wrapped? {
try self.lock.withLock {
if let value = self.underlying {
return value
}
let value = try body()
self.underlying = value
return value
}
set {
self.lock.withLock {
if var value = self.underlying {
value[keyPath: keyPath] = newValue
}
}

/// Atomically computes and caches an optional value if not already present.
///
/// If the box already contains a non-nil value, that value is returned immediately.
/// Otherwise, the provided closure is executed to compute the value, which is then
/// stored and returned. This method is only available when the wrapped value type is optional.
///
/// If the returned value is `nil` subsequent calls to `memoize` or `memoizeOptional` will
/// re-execute the closure.
///
/// - Parameter body: A closure that computes the optional value to store if none exists.
/// - Returns: The cached value or the newly computed value (which may be nil).
/// - Throws: Any error thrown by the computation closure.
@discardableResult
public func memoizeOptional<Wrapped>(body: () throws -> Wrapped?) rethrows -> Wrapped? where Value == Wrapped? {
try self.lock.withLock {
if let value = self.underlying {
return value
}
let value = try body()
self.underlying = value
return value
}
}
}

extension ThreadSafeBox where Value == Int {
/// Atomically increments the stored integer value by 1.
///
/// This method is only available when the wrapped value type is Int.
public func increment() {
self.lock.withLock {
if let value = self.underlying {
self.underlying = value + 1
}
self.underlying = self.underlying + 1
}
}

/// Atomically decrements the stored integer value by 1.
///
/// This method is only available when the wrapped value type is Int.
public func decrement() {
self.lock.withLock {
if let value = self.underlying {
self.underlying = value - 1
}
self.underlying = self.underlying - 1
}
}
}

extension ThreadSafeBox where Value == String {
/// Atomically appends a string to the stored string value.
///
/// This method is only available when the wrapped value type is String.
///
/// - Parameter value: The string to append to the current stored value.
public func append(_ value: String) {
self.mutate { existingValue in
if let existingValue {
return existingValue + value
} else {
return value
}
existingValue + value
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Basics/Observability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
}

var errorsReported: Bool {
self._errorsReported.get() ?? false
self._errorsReported.get()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
}

/// The build description resulting from planing.
private let buildDescription = ThreadSafeBox<BuildDescription>()
private let buildDescription = AsyncThrowingValueMemoizer<BuildDescription>()

/// The loaded package graph.
private let packageGraph = ThreadSafeBox<ModulesGraph>()
private let packageGraph = AsyncThrowingValueMemoizer<ModulesGraph>()

/// File system to operate on.
private var fileSystem: Basics.FileSystem {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Build/ClangSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public enum ClangSupport {
let features: [Feature]
}

private static var cachedFeatures = ThreadSafeBox<Features>()
private static var cachedFeatures = ThreadSafeBox<Features?>()

public static func supportsFeature(name: String, toolchain: PackageModel.Toolchain) throws -> Bool {
let features = try cachedFeatures.memoize {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Build/LLBuildProgressTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public final class BuildExecutionContext {

// MARK: - Private

private var indexStoreAPICache = ThreadSafeBox<Result<IndexStoreAPI, Error>>()
private var indexStoreAPICache = ThreadSafeBox<Result<IndexStoreAPI, Error>?>()

/// Reference to the index store API.
var indexStoreAPI: Result<IndexStoreAPI, Error> {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Commands/SwiftTestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ final class ParallelTestRunner {
}
self.finishedTests.enqueue(TestResult(
unitTest: test,
output: output.get() ?? "",
output: output.get(),
success: result != .failure,
duration: duration
))
Expand Down
8 changes: 5 additions & 3 deletions Sources/DriverSupport/DriverSupportUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import class TSCBasic.Process
import struct TSCBasic.ProcessResult

public enum DriverSupport {
private static var flagsMap = ThreadSafeBox<[String: Set<String>]>()
private static var flagsMap = ThreadSafeBox<[String: Set<String>]>([:])

/// This checks _frontend_ supported flags, which are not necessarily supported in the driver.
public static func checkSupportedFrontendFlags(
Expand All @@ -29,8 +29,9 @@ public enum DriverSupport {
) -> Bool {
let trimmedFlagSet = Set(flags.map { $0.trimmingCharacters(in: ["-"]) })
let swiftcPathString = toolchain.swiftCompilerPath.pathString
let entry = flagsMap.get()

if let entry = flagsMap.get(), let cachedSupportedFlagSet = entry[swiftcPathString + "-frontend"] {
if let cachedSupportedFlagSet = entry[swiftcPathString + "-frontend"] {
return cachedSupportedFlagSet.intersection(trimmedFlagSet) == trimmedFlagSet
}
do {
Expand Down Expand Up @@ -63,8 +64,9 @@ public enum DriverSupport {
) -> Bool {
let trimmedFlagSet = Set(flags.map { $0.trimmingCharacters(in: ["-"]) })
let swiftcPathString = toolchain.swiftCompilerPath.pathString
let entry = flagsMap.get()

if let entry = flagsMap.get(), let cachedSupportedFlagSet = entry[swiftcPathString + "-driver"] {
if let cachedSupportedFlagSet = entry[swiftcPathString + "-driver"] {
return cachedSupportedFlagSet.intersection(trimmedFlagSet) == trimmedFlagSet
}
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
private let ftsLock = NSLock()
// FTS not supported on some platforms; the code falls back to "slow path" in that case
// marked internal for testing
internal let useSearchIndices = ThreadSafeBox<Bool>()
internal let useSearchIndices = ThreadSafeBox<Bool>(false)

// Targets have in-memory trie in addition to SQLite FTS as optimization
private let targetTrie = Trie<CollectionPackage>()
Expand Down Expand Up @@ -725,9 +725,10 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
private func shouldUseSearchIndices() throws -> Bool {
// Make sure createSchemaIfNecessary is called and useSearchIndices is set before reading it
try self.withDB { _ in
self.useSearchIndices.get() ?? false
self.useSearchIndices.get()
}
}

internal func populateTargetTrie() async throws {
try await withCheckedThrowingContinuation { continuation in
self.populateTargetTrie(callback: {
Expand Down
Loading
Loading