Skip to content

Commit 632a837

Browse files
authored
Enable strict concurrency checking in CI (#132)
1 parent 7c5943f commit 632a837

18 files changed

+234
-135
lines changed

Package.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ let package = Package(
5757
),
5858
.target(
5959
name: "_TracingBenchmarkTools",
60-
dependencies: [],
60+
dependencies: [
61+
.target(name: "Instrumentation"),
62+
],
6163
exclude: ["README_SWIFT.md"]
6264
),
6365
]

Sources/Instrumentation/InstrumentationSystem.swift

+48-26
Original file line numberDiff line numberDiff line change
@@ -24,56 +24,78 @@ import ServiceContextModule
2424
/// ``instrument``: Returns whatever you passed to ``bootstrap(_:)`` as an ``Instrument``.
2525
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext
2626
public enum InstrumentationSystem {
27-
private static let lock = ReadWriteLock()
28-
private static var _instrument: Instrument = NoOpInstrument()
29-
private static var isInitialized = false
27+
/// Marked as @unchecked Sendable due to the synchronization being
28+
/// performed manually using locks.
29+
private final class Storage: @unchecked Sendable {
30+
private let lock = ReadWriteLock()
31+
private var _instrument: Instrument = NoOpInstrument()
32+
private var _isInitialized = false
33+
34+
func bootstrap(_ instrument: Instrument) {
35+
self.lock.withWriterLock {
36+
precondition(
37+
!self._isInitialized, """
38+
InstrumentationSystem can only be initialized once per process. Consider using MultiplexInstrument if
39+
you need to use multiple instruments.
40+
"""
41+
)
42+
self._instrument = instrument
43+
self._isInitialized = true
44+
}
45+
}
46+
47+
func bootstrapInternal(_ instrument: Instrument?) {
48+
self.lock.withWriterLock {
49+
self._instrument = instrument ?? NoOpInstrument()
50+
}
51+
}
52+
53+
var instrument: Instrument {
54+
self.lock.withReaderLock { self._instrument }
55+
}
56+
57+
func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
58+
self.lock.withReaderLock {
59+
if let multiplex = self._instrument as? MultiplexInstrument {
60+
return multiplex.firstInstrument(where: predicate)
61+
} else if predicate(self._instrument) {
62+
return self._instrument
63+
} else {
64+
return nil
65+
}
66+
}
67+
}
68+
}
69+
70+
private static let shared = Storage()
3071

3172
/// Globally select the desired ``Instrument`` implementation.
3273
///
3374
/// - Parameter instrument: The ``Instrument`` you want to share globally within your system.
3475
/// - Warning: Do not call this method more than once. This will lead to a crash.
3576
public static func bootstrap(_ instrument: Instrument) {
36-
self.lock.withWriterLock {
37-
precondition(
38-
!self.isInitialized, """
39-
InstrumentationSystem can only be initialized once per process. Consider using MultiplexInstrument if
40-
you need to use multiple instruments.
41-
"""
42-
)
43-
self._instrument = instrument
44-
self.isInitialized = true
45-
}
77+
self.shared.bootstrap(instrument)
4678
}
4779

4880
/// For testing scenarios one may want to set instruments multiple times, rather than the set-once semantics enforced by ``bootstrap(_:)``.
4981
///
5082
/// - Parameter instrument: the instrument to boostrap the system with, if `nil` the ``NoOpInstrument`` is bootstrapped.
5183
internal static func bootstrapInternal(_ instrument: Instrument?) {
52-
self.lock.withWriterLock {
53-
self._instrument = instrument ?? NoOpInstrument()
54-
}
84+
self.shared.bootstrapInternal(instrument)
5585
}
5686

5787
/// Returns the globally configured ``Instrument``.
5888
///
5989
/// Defaults to a no-op ``Instrument`` if ``bootstrap(_:)`` wasn't called before.
6090
public static var instrument: Instrument {
61-
self.lock.withReaderLock { self._instrument }
91+
shared.instrument
6292
}
6393
}
6494

6595
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext
6696
extension InstrumentationSystem {
6797
/// INTERNAL API: Do Not Use
6898
public static func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
69-
self.lock.withReaderLock {
70-
if let multiplex = self._instrument as? MultiplexInstrument {
71-
return multiplex.firstInstrument(where: predicate)
72-
} else if predicate(self._instrument) {
73-
return self._instrument
74-
} else {
75-
return nil
76-
}
77-
}
99+
self.shared._findInstrument(where: predicate)
78100
}
79101
}

Sources/Instrumentation/Locks.swift

+23-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import Glibc
3737
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
3838
/// of lock is safe to use with `libpthread`-based threading models, such as the
3939
/// one used by NIO.
40-
internal final class ReadWriteLock {
40+
@_spi(Locking) /* Use the `package` access modifier once min Swift version is increased. */
41+
public final class ReadWriteLock {
4142
private let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = UnsafeMutablePointer.allocate(capacity: 1)
4243

4344
/// Create a new lock.
@@ -90,7 +91,7 @@ extension ReadWriteLock {
9091
/// - Parameter body: The block to execute while holding the lock.
9192
/// - Returns: The value returned by the block.
9293
@inlinable
93-
func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
94+
public func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
9495
self.lockRead()
9596
defer {
9697
self.unlock()
@@ -107,11 +108,30 @@ extension ReadWriteLock {
107108
/// - Parameter body: The block to execute while holding the lock.
108109
/// - Returns: The value returned by the block.
109110
@inlinable
110-
func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
111+
public func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
111112
self.lockWrite()
112113
defer {
113114
self.unlock()
114115
}
115116
return try body()
116117
}
117118
}
119+
120+
/// A wrapper providing locked access to a value.
121+
///
122+
/// Marked as @unchecked Sendable due to the synchronization being
123+
/// performed manually using locks.
124+
@_spi(Locking) /* Use the `package` access modifier once min Swift version is increased. */
125+
public final class LockedValueBox<Value: Sendable>: @unchecked Sendable {
126+
private let lock = ReadWriteLock()
127+
private var value: Value
128+
public init(_ value: Value) {
129+
self.value = value
130+
}
131+
132+
public func withValue<R>(_ work: (inout Value) throws -> R) rethrows -> R {
133+
try self.lock.withWriterLock {
134+
try work(&self.value)
135+
}
136+
}
137+
}

Sources/Tracing/NoOpTracer.swift

+9-8
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ public struct NoOpTracer: LegacyTracer {
2323

2424
public init() {}
2525

26-
public func startAnySpan<Instant: TracerInstant>(_ operationName: String,
27-
context: @autoclosure () -> ServiceContext,
28-
ofKind kind: SpanKind,
29-
at instant: @autoclosure () -> Instant,
30-
function: String,
31-
file fileID: String,
32-
line: UInt) -> any Tracing.Span
33-
{
26+
public func startAnySpan<Instant: TracerInstant>(
27+
_ operationName: String,
28+
context: @autoclosure () -> ServiceContext,
29+
ofKind kind: SpanKind,
30+
at instant: @autoclosure () -> Instant,
31+
function: String,
32+
file fileID: String,
33+
line: UInt
34+
) -> any Tracing.Span {
3435
NoOpSpan(context: context())
3536
}
3637

Sources/Tracing/SpanProtocol.swift

+14-10
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ public protocol Span: _SwiftTracingSendableSpan {
7979
/// - error: The error to be recorded
8080
/// - attributes: Additional attributes describing the error
8181
/// - instant: the time instant at which the event occurred
82-
func recordError<Instant: TracerInstant>(_ error: Error,
83-
attributes: SpanAttributes,
84-
at instant: @autoclosure () -> Instant)
82+
func recordError<Instant: TracerInstant>(
83+
_ error: Error,
84+
attributes: SpanAttributes,
85+
at instant: @autoclosure () -> Instant
86+
)
8587

8688
/// The attributes describing this `Span`.
8789
var attributes: SpanAttributes {
@@ -185,18 +187,20 @@ public struct SpanEvent: Equatable {
185187
/// - name: The human-readable name of this event.
186188
/// - attributes: attributes describing this event. Defaults to no attributes.
187189
/// - instant: the time instant at which the event occurred
188-
public init<Instant: TracerInstant>(name: String,
189-
at instant: @autoclosure () -> Instant,
190-
attributes: SpanAttributes = [:])
191-
{
190+
public init<Instant: TracerInstant>(
191+
name: String,
192+
at instant: @autoclosure () -> Instant,
193+
attributes: SpanAttributes = [:]
194+
) {
192195
self.name = name
193196
self.attributes = attributes
194197
self.nanosecondsSinceEpoch = instant().nanosecondsSinceEpoch
195198
}
196199

197-
public init(name: String,
198-
attributes: SpanAttributes = [:])
199-
{
200+
public init(
201+
name: String,
202+
attributes: SpanAttributes = [:]
203+
) {
200204
self.name = name
201205
self.attributes = attributes
202206
self.nanosecondsSinceEpoch = DefaultTracerClock.now.nanosecondsSinceEpoch

Sources/_TracingBenchmarkTools/ArgParser.swift

+10-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
//===----------------------------------------------------------------------===//
2626

2727
import Foundation
28+
@_spi(Locking) import Instrumentation
2829

2930
enum ArgumentError: Error {
3031
case missingValue(String)
@@ -81,7 +82,7 @@ class ArgumentParser<U> {
8182
private var arguments: [Argument] = []
8283
private let programName: String = {
8384
// Strip full path from the program name.
84-
let r = CommandLine.arguments[0].reversed()
85+
let r = ProcessInfo.processInfo.arguments[0].reversed()
8586
let ss = r[r.startIndex ..< (r.firstIndex(of: "/") ?? r.endIndex)]
8687
return String(ss.reversed())
8788
}()
@@ -150,10 +151,14 @@ class ArgumentParser<U> {
150151
try self.arguments.forEach { try $0.apply() } // type-check and store values
151152
return self.result
152153
} catch let error as ArgumentError {
153-
fputs("error: \(error)\n", stderr)
154+
lockedStderr.withValue { stderr in
155+
_ = fputs("error: \(error)\n", stderr)
156+
}
154157
exit(1)
155158
} catch {
156-
fflush(stdout)
159+
lockedStdout.withValue { stdout in
160+
_ = fflush(stdout)
161+
}
157162
fatalError("\(error)")
158163
}
159164
}
@@ -173,7 +178,8 @@ class ArgumentParser<U> {
173178
/// the supported argument syntax.
174179
private func parseArgs() throws {
175180
// For each argument we are passed...
176-
for arg in CommandLine.arguments[1 ..< CommandLine.arguments.count] {
181+
let arguments = ProcessInfo.processInfo.arguments
182+
for arg in arguments[1 ..< arguments.count] {
177183
// If the argument doesn't match the optional argument pattern. Add
178184
// it to the positional argument list and continue...
179185
if !arg.starts(with: "-") {

Sources/_TracingBenchmarkTools/BenchmarkCategory.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
//
1919
//===----------------------------------------------------------------------===//
2020

21-
public enum BenchmarkCategory: String {
21+
public enum BenchmarkCategory: String, Sendable {
2222
// Most benchmarks are assumed to be "stable" and will be regularly tracked at
2323
// each commit. A handful may be marked unstable if continually tracking them is
2424
// counterproductive.

Sources/_TracingBenchmarkTools/BenchmarkTools.swift

+24-11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import Glibc
2323
#else
2424
import Darwin
2525
#endif
26+
import Foundation
27+
@_spi(Locking) import Instrumentation
2628

2729
extension BenchmarkCategory: CustomStringConvertible {
2830
public var description: String {
@@ -36,7 +38,7 @@ extension BenchmarkCategory: Comparable {
3638
}
3739
}
3840

39-
public struct BenchmarkPlatformSet: OptionSet {
41+
public struct BenchmarkPlatformSet: OptionSet, Sendable {
4042
public let rawValue: Int
4143

4244
public init(rawValue: Int) {
@@ -59,12 +61,12 @@ public struct BenchmarkPlatformSet: OptionSet {
5961
}
6062
}
6163

62-
public struct BenchmarkInfo {
64+
public struct BenchmarkInfo: Sendable {
6365
/// The name of the benchmark that should be displayed by the harness.
6466
public var name: String
6567

6668
/// Shadow static variable for runFunction.
67-
private var _runFunction: (Int) -> Void
69+
private var _runFunction: @Sendable (Int) -> Void
6870

6971
/// A function that invokes the specific benchmark routine.
7072
public var runFunction: ((Int) -> Void)? {
@@ -83,7 +85,7 @@ public struct BenchmarkInfo {
8385
private var unsupportedPlatforms: BenchmarkPlatformSet
8486

8587
/// Shadow variable for setUpFunction.
86-
private var _setUpFunction: (() -> Void)?
88+
private var _setUpFunction: (@Sendable () -> Void)?
8789

8890
/// An optional function that if non-null is run before benchmark samples
8991
/// are timed.
@@ -95,7 +97,7 @@ public struct BenchmarkInfo {
9597
}
9698

9799
/// Shadow static variable for computed property tearDownFunction.
98-
private var _tearDownFunction: (() -> Void)?
100+
private var _tearDownFunction: (@Sendable () -> Void)?
99101

100102
/// An optional function that if non-null is run after samples are taken.
101103
public var tearDownFunction: (() -> Void)? {
@@ -108,9 +110,9 @@ public struct BenchmarkInfo {
108110
public var legacyFactor: Int?
109111

110112
public init(
111-
name: String, runFunction: @escaping (Int) -> Void, tags: [BenchmarkCategory],
112-
setUpFunction: (() -> Void)? = nil,
113-
tearDownFunction: (() -> Void)? = nil,
113+
name: String, runFunction: @escaping @Sendable (Int) -> Void, tags: [BenchmarkCategory],
114+
setUpFunction: (@Sendable () -> Void)? = nil,
115+
tearDownFunction: (@Sendable () -> Void)? = nil,
114116
unsupportedPlatforms: BenchmarkPlatformSet = [],
115117
legacyFactor: Int? = nil
116118
) {
@@ -168,17 +170,28 @@ struct LFSR {
168170
}
169171
}
170172

171-
var lfsrRandomGenerator = LFSR()
173+
let lfsrRandomGenerator: LockedValueBox<LFSR> = .init(LFSR())
172174

173175
// Start the generator from the beginning
174176
public func SRand() {
175-
lfsrRandomGenerator = LFSR()
177+
lfsrRandomGenerator.withValue { lfsr in
178+
lfsr = LFSR()
179+
}
176180
}
177181

178182
public func Random() -> Int64 {
179-
lfsrRandomGenerator.randInt()
183+
lfsrRandomGenerator.withValue { lfsr in
184+
lfsr.randInt()
185+
}
180186
}
181187

188+
// Can't access stdout/stderr directly in strict concurrency checking mode.
189+
// let lockedStdout = LockedValueBox(stdout)
190+
// let lockedStderr = LockedValueBox(stderr)
191+
192+
let lockedStdout = LockedValueBox(fdopen(STDOUT_FILENO, "w"))
193+
let lockedStderr = LockedValueBox(fdopen(STDERR_FILENO, "w"))
194+
182195
@inlinable
183196
@inline(__always)
184197
public func CheckResults(

0 commit comments

Comments
 (0)