Skip to content

Commit 95d9871

Browse files
authored
Adopt Atomic. (#1599)
This PR moves us from `Mutex` to `Atomic` for scalar integer values that don't need a full mutex guarding them. On Darwin, we cannot use types from the `Synchronization` module because we need to back-deploy earlier than they are available, so we simulate a subset of the `Atomic` API using C atomic intrinsics provided by clang and bubbled up through Stubs.h. Unlike the real `Atomic` type, we heap-allocate our storage, but we're heap-allocating the current mutex so that isn't a performance regression. I have only implemented the set of functionality we're actually using; adding more functionality isn't hard, but is beyond the scope of this PR. In particular, I have not attempted to implement/use any atomic orderings other than `.sequentiallyConsistent`. We can revise orderings later on a case-by-case basis. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 4b38ab0 commit 95d9871

File tree

17 files changed

+285
-155
lines changed

17 files changed

+285
-155
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private import Synchronization
3030
/// ``ABI/v0/entryPoint-swift.type.property`` to get a reference to an
3131
/// ABI-stable version of this function.
3232
func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Handler?) async -> CInt {
33-
let exitCode = Mutex(EXIT_SUCCESS)
33+
let exitCode = Atomic(EXIT_SUCCESS)
3434

3535
do {
3636
#if !SWT_NO_EXIT_TESTS
@@ -47,9 +47,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
4747
// Set up the event handler.
4848
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
4949
if case let .issueRecorded(issue) = event.kind, issue.isFailure {
50-
exitCode.withLock { exitCode in
51-
exitCode = EXIT_FAILURE
52-
}
50+
exitCode.store(EXIT_FAILURE, ordering: .sequentiallyConsistent)
5351
}
5452
oldEventHandler(event, context)
5553
}
@@ -131,23 +129,21 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
131129
// the caller (assumed to be Swift Package Manager) can implement special
132130
// handling.
133131
if tests.isEmpty {
134-
exitCode.withLock { exitCode in
135-
if exitCode == EXIT_SUCCESS {
136-
exitCode = EXIT_NO_TESTS_FOUND
137-
}
138-
}
132+
_ = exitCode.compareExchange(
133+
expected: EXIT_SUCCESS,
134+
desired: EXIT_NO_TESTS_FOUND,
135+
ordering: .sequentiallyConsistent
136+
)
139137
}
140138
} catch {
141139
#if !SWT_NO_FILE_IO
142140
try? FileHandle.stderr.write("\(String(describingForTest: error))\n")
143141
#endif
144142

145-
exitCode.withLock { exitCode in
146-
exitCode = EXIT_FAILURE
147-
}
143+
exitCode.store(EXIT_FAILURE, ordering: .sequentiallyConsistent)
148144
}
149145

150-
return exitCode.rawValue
146+
return exitCode.load(ordering: .sequentiallyConsistent)
151147
}
152148

153149
// MARK: - Listing tests

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ add_library(Testing
8282
SourceAttribution/SourceLocation.swift
8383
SourceAttribution/SourceLocation+Macro.swift
8484
Support/Additions/ArrayAdditions.swift
85+
Support/Additions/AtomicAdditions.swift
8586
Support/Additions/CollectionDifferenceAdditions.swift
8687
Support/Additions/CommandLineAdditions.swift
8788
Support/Additions/CopyableAdditions.swift

Sources/Testing/Issues/Confirmation.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct Confirmation: Sendable {
2020
///
2121
/// This property is fileprivate because it may be mutated asynchronously and
2222
/// callers may be tempted to use it in ways that result in data races.
23-
fileprivate var count = Allocated(Mutex(0))
23+
fileprivate var count = Allocated(Atomic(0))
2424

2525
/// Confirm this confirmation.
2626
///
@@ -31,7 +31,7 @@ public struct Confirmation: Sendable {
3131
/// directly.
3232
public func confirm(count: Int = 1) {
3333
precondition(count > 0)
34-
self.count.value.add(count)
34+
self.count.value.add(count, ordering: .sequentiallyConsistent)
3535
}
3636
}
3737

@@ -181,7 +181,7 @@ public func confirmation<R>(
181181
) async rethrows -> R {
182182
let confirmation = Confirmation()
183183
defer {
184-
let actualCount = confirmation.count.value.rawValue
184+
let actualCount = confirmation.count.value.load(ordering: .sequentiallyConsistent)
185185
if !expectedCount.contains(actualCount) {
186186
let issue = Issue(
187187
kind: .confirmationMiscounted(actual: actualCount, expected: expectedCount),

Sources/Testing/Issues/KnownIssue.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct KnownIssueScope: Sendable {
3333
var matcher: Matcher
3434

3535
/// The number of issues this scope and its ancestors have matched.
36-
fileprivate let matchCounter: Allocated<Mutex<Int>>
36+
fileprivate let matchCounter: Allocated<Atomic<Int>>
3737

3838
/// Create a new ``KnownIssueScope`` by combining a new issue matcher with
3939
/// any already-active scope.
@@ -46,7 +46,7 @@ struct KnownIssueScope: Sendable {
4646
/// - context: The context to be associated with issues matched by
4747
/// `issueMatcher`.
4848
init(parent: KnownIssueScope? = .current, issueMatcher: @escaping KnownIssueMatcher, context: Issue.KnownIssueContext) {
49-
let matchCounter = Allocated(Mutex(0))
49+
let matchCounter = Allocated(Atomic(0))
5050
self.matchCounter = matchCounter
5151
matcher = { issue in
5252
let matchedContext = if issueMatcher(issue) {
@@ -55,7 +55,7 @@ struct KnownIssueScope: Sendable {
5555
parent?.matcher(issue)
5656
}
5757
if matchedContext != nil {
58-
matchCounter.value.increment()
58+
matchCounter.value.add(1, ordering: .sequentiallyConsistent)
5959
}
6060
return matchedContext
6161
}
@@ -110,8 +110,8 @@ private func _matchError(_ error: any Error, in scope: KnownIssueScope, comment:
110110
/// function.
111111
/// - sourceLocation: The source location to which the issue should be
112112
/// attributed.
113-
private func _handleMiscount(by matchCounter: Allocated<Mutex<Int>>, comment: Comment?, sourceLocation: SourceLocation) {
114-
if matchCounter.value.rawValue == 0 {
113+
private func _handleMiscount(by matchCounter: Allocated<Atomic<Int>>, comment: Comment?, sourceLocation: SourceLocation) {
114+
if matchCounter.value.load(ordering: .sequentiallyConsistent) == 0 {
115115
let issue = Issue(
116116
kind: .knownIssueNotRecorded,
117117
comments: Array(comment),

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ extension Configuration {
137137
/// passed to `_removeFromAll(identifiedBy:)`` to unregister it.
138138
private func _addToAll() -> UInt64 {
139139
if eventHandlingOptions.isExpectationCheckedEventEnabled {
140-
Self._deliverExpectationCheckedEventsCount.increment()
140+
Self._deliverExpectationCheckedEventsCount.add(1, ordering: .sequentiallyConsistent)
141141
}
142142
return Self._all.withLock { all in
143143
let id = all.nextID
@@ -157,7 +157,7 @@ extension Configuration {
157157
all.instances.removeValue(forKey: id)
158158
}
159159
if let configuration, configuration.eventHandlingOptions.isExpectationCheckedEventEnabled {
160-
Self._deliverExpectationCheckedEventsCount.decrement()
160+
Self._deliverExpectationCheckedEventsCount.subtract(1, ordering: .sequentiallyConsistent)
161161
}
162162
}
163163

@@ -182,7 +182,7 @@ extension Configuration {
182182
/// An atomic counter that tracks the number of "current" configurations that
183183
/// have set ``EventHandlingOptions/isExpectationCheckedEventEnabled`` to
184184
/// `true`.
185-
private static let _deliverExpectationCheckedEventsCount = Mutex(0)
185+
private static let _deliverExpectationCheckedEventsCount = Atomic(0)
186186

187187
/// Whether or not events of the kind
188188
/// ``Event/Kind-swift.enum/expectationChecked(_:)`` should be delivered to
@@ -194,7 +194,7 @@ extension Configuration {
194194
/// ``Configuration/EventHandlingOptions/isExpectationCheckedEventEnabled``
195195
/// property.
196196
static var deliverExpectationCheckedEvents: Bool {
197-
_deliverExpectationCheckedEventsCount.rawValue > 0
197+
_deliverExpectationCheckedEventsCount.load(ordering: .sequentiallyConsistent) > 0
198198
}
199199
}
200200

Sources/Testing/Running/Runner.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,10 @@ extension Runner {
454454
#endif
455455

456456
// Track whether or not any issues were recorded across the entire run.
457-
let issueRecorded = Mutex(false)
457+
let issueRecorded = Atomic(false)
458458
runner.configuration.eventHandler = { [eventHandler = runner.configuration.eventHandler] event, context in
459459
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
460-
issueRecorded.withLock { issueRecorded in
461-
issueRecorded = true
462-
}
460+
issueRecorded.store(true, ordering: .sequentiallyConsistent)
463461
}
464462
eventHandler(event, context)
465463
}
@@ -521,18 +519,16 @@ extension Runner {
521519
case nil:
522520
true
523521
case .untilIssueRecorded:
524-
!issueRecorded.rawValue
522+
!issueRecorded.load(ordering: .sequentiallyConsistent)
525523
case .whileIssueRecorded:
526-
issueRecorded.rawValue
524+
issueRecorded.load(ordering: .sequentiallyConsistent)
527525
}
528526
guard shouldContinue else {
529527
break
530528
}
531529

532530
// Reset the run-wide "issue was recorded" flag for this iteration.
533-
issueRecorded.withLock { issueRecorded in
534-
issueRecorded = false
535-
}
531+
issueRecorded.store(false, ordering: .sequentiallyConsistent)
536532
}
537533
}
538534
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
internal import _TestingInternals
12+
13+
#if canImport(Synchronization)
14+
internal import Synchronization
15+
#endif
16+
17+
#if SWT_TARGET_OS_APPLE
18+
/// A type that replicates the interface of ``Synchronization/Atomic``.
19+
///
20+
/// This type is used on Apple platforms because our deployment target there is
21+
/// earlier than the availability of the ``Synchronization/Atomic`` type. It
22+
/// replicates the interface of that type but is implemented differently (using
23+
/// a heap-allocated value) with support for only those atomic operations that
24+
/// we need to use in the testing library.
25+
///
26+
/// Since we don't try to implement the complete ``Synchronization/AtomicRepresentable``
27+
/// protocol, this implementation only supports using a few types that are
28+
/// actually in use in the testing library.
29+
struct Atomic<Value>: ~Copyable {
30+
/// Storage for the underlying atomic value.
31+
private nonisolated(unsafe) var _address: UnsafeMutablePointer<Value>
32+
33+
init(_ value: consuming sending Value) {
34+
_address = .allocate(capacity: 1)
35+
_address.initialize(to: value)
36+
}
37+
38+
deinit {
39+
_address.deinitialize(count: 1)
40+
_address.deallocate()
41+
}
42+
43+
/// The orderings supported by this type.
44+
///
45+
/// At this time, we only implement sequentially consistent ordering. For more
46+
/// information about atomic operation ordering, see [`AtomicUpdateOrdering`](https://developer.apple.com/documentation/synchronization/atomicupdateordering).
47+
enum Ordering {
48+
case sequentiallyConsistent
49+
}
50+
}
51+
52+
extension Atomic: Sendable where Value: Sendable {}
53+
54+
// MARK: - Atomic<Bool>
55+
56+
extension Atomic where Value == Bool {
57+
func load(ordering: Ordering) -> Value {
58+
swt_atomicLoad(_address)
59+
}
60+
61+
func store(_ desired: consuming Value, ordering: Ordering) {
62+
swt_atomicStore(_address, desired)
63+
}
64+
65+
func compareExchange(expected: consuming Value, desired: consuming Value) -> (exchanged: Bool, original: Value) {
66+
var expected = expected
67+
let exchanged = swt_atomicCompareExchange(_address, &expected, desired)
68+
return (exchanged, expected)
69+
}
70+
}
71+
72+
// MARK: - Atomic<CInt>
73+
74+
extension Atomic where Value == CInt {
75+
func load(ordering: Ordering) -> Value {
76+
return swt_atomicLoad(_address)
77+
}
78+
79+
func store(_ desired: consuming Value, ordering: Ordering) {
80+
swt_atomicStore(_address, desired)
81+
}
82+
83+
func compareExchange(expected: consuming Value, desired: consuming Value, ordering: Ordering) -> (exchanged: Bool, original: Value) {
84+
var expected = expected
85+
let exchanged = swt_atomicCompareExchange(_address, &expected, desired)
86+
return (exchanged, expected)
87+
}
88+
}
89+
90+
// MARK: - Atomic<Int>
91+
92+
extension Atomic where Value == Int {
93+
func load(ordering: Ordering) -> Value {
94+
swt_atomicLoad(_address)
95+
}
96+
97+
func store(_ desired: consuming Value, ordering: Ordering) {
98+
swt_atomicStore(_address, desired)
99+
}
100+
101+
func compareExchange(expected: consuming Value, desired: consuming Value, ordering: Ordering) -> (exchanged: Bool, original: Value) {
102+
var expected = expected
103+
let exchanged = swt_atomicCompareExchange(_address, &expected, desired)
104+
return (exchanged, expected)
105+
}
106+
107+
@discardableResult
108+
func add(_ operand: Value, ordering: Ordering) -> (oldValue: Value, newValue: Value) {
109+
while true {
110+
let oldValue = load(ordering: ordering)
111+
let newValue = oldValue + operand
112+
if compareExchange(expected: oldValue, desired: newValue, ordering: ordering).exchanged {
113+
return (oldValue, newValue)
114+
}
115+
}
116+
}
117+
118+
@discardableResult
119+
func subtract(_ operand: Value, ordering: Ordering) -> (oldValue: Value, newValue: Value) {
120+
add(-operand, ordering: ordering)
121+
}
122+
}
123+
#endif

Sources/Testing/Support/Additions/MutexAdditions.swift

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -138,42 +138,6 @@ extension Mutex where Value: Copyable {
138138

139139
// MARK: - Additions
140140

141-
extension Mutex where Value: AdditiveArithmetic & Sendable {
142-
/// Add something to the current wrapped value of this instance.
143-
///
144-
/// - Parameters:
145-
/// - addend: The value to add.
146-
///
147-
/// - Returns: The sum of ``rawValue`` and `addend`.
148-
@discardableResult func add(_ addend: Value) -> Value {
149-
withLock { rawValue in
150-
let result = rawValue + addend
151-
rawValue = result
152-
return result
153-
}
154-
}
155-
}
156-
157-
extension Mutex where Value: Numeric & Sendable {
158-
/// Increment the current wrapped value of this instance.
159-
///
160-
/// - Returns: The sum of ``rawValue`` and `1`.
161-
///
162-
/// This function is exactly equivalent to `add(1)`.
163-
@discardableResult func increment() -> Value {
164-
add(1)
165-
}
166-
167-
/// Decrement the current wrapped value of this instance.
168-
///
169-
/// - Returns: The sum of ``rawValue`` and `-1`.
170-
///
171-
/// This function is exactly equivalent to `add(-1)`.
172-
@discardableResult func decrement() -> Value {
173-
add(-1)
174-
}
175-
}
176-
177141
extension Mutex where Value: ~Copyable {
178142
/// Initialize an instance of this type with a raw value of `nil`.
179143
init<V>() where Value == V?, V: ~Copyable {

Sources/_TestingInternals/include/Defines.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
#define SWT_IMPORT_FROM_STDLIB SWT_EXTERN
2727
#endif
2828

29+
/// A macro that helps when concatenating two parts of a symbol name.
30+
#define __SWT_CONCAT(A, B) A ## B
31+
#define SWT_CONCAT(A, B) __SWT_CONCAT(A, B)
32+
2933
/// An attribute that marks some value as being `Sendable` in Swift.
3034
#define SWT_SENDABLE __attribute__((swift_attr("@Sendable")))
3135

0 commit comments

Comments
 (0)