Skip to content

Commit 2327cdc

Browse files
committed
Implement Statistic with atomics so it's thread safe
Prevents false positives in TSAN which obscure other real issues.
1 parent 316345e commit 2327cdc

File tree

1 file changed

+52
-22
lines changed

1 file changed

+52
-22
lines changed

Sources/SWBUtil/Statistics.swift

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,23 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Synchronization
14+
1315
/// A scope to contain a group of statistics.
1416
public final class StatisticsGroup: Sendable {
1517
/// The name for this group.
1618
public let name: String
1719

18-
private let _statistics = LockedValue<[Statistic]>([])
20+
private let _statistics = LockedValue<[any _StatisticBackend]>([])
1921

2022
/// The list of statistics in the group.
21-
public var statistics: [Statistic] { return _statistics.withLock { $0 } }
23+
public var statistics: [any _StatisticBackend] { return _statistics.withLock { $0 } }
2224

2325
public init(_ name: String) {
2426
self.name = name
2527
}
2628

27-
public func register(_ statistic: Statistic) {
29+
public func register(_ statistic: any _StatisticBackend) {
2830
_statistics.withLock { $0.append(statistic) }
2931
}
3032

@@ -40,20 +42,16 @@ public final class StatisticsGroup: Sendable {
4042
///
4143
/// Currently statistics are always integers and are not thread safe (unless building in TSan mode); clients should implement their own locking if an accurate count is required.
4244
// FIXME: This should unconditionally be implemented using atomics, not conditionally be using a queue based on TSan...
43-
public final class Statistic: @unchecked Sendable {
45+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2, *)
46+
public final class _Statistic: @unchecked Sendable, _StatisticBackend {
4447
/// The name of the statistics.
4548
public let name: String
4649

4750
/// The description of the statistics.
4851
public let description: String
4952

50-
#if ENABLE_THREAD_SANITIZER
51-
/// Queue to serialize access to the statistic value.
52-
let _queue = SWBQueue(label: "com.apple.dt.SWBUtil.Statistics")
53-
#endif
54-
5553
/// The value of the statistic.
56-
var _value: Int = 0
54+
private let _value = Atomic<Int>(0)
5755

5856
public init(_ name: String, _ description: String, _ group: StatisticsGroup = allStatistics) {
5957
self.name = name
@@ -64,28 +62,19 @@ public final class Statistic: @unchecked Sendable {
6462

6563
/// Get the current value of the statistic.
6664
public var value: Int {
67-
return sync { _value }
65+
return _value.load(ordering: .relaxed)
6866
}
6967

7068
/// Increment the statistic.
7169
public func increment(_ n: Int = 1) {
72-
sync { _value += n }
70+
_value.wrappingAdd(n, ordering: .relaxed)
7371
}
7472

7573
/// Zero all of the statistics.
7674
///
7775
/// This is useful when using statistics to probe program behavior from within tests, and the test can guarantee no concurrent access.
7876
public func zero() {
79-
sync { _value = 0 }
80-
}
81-
82-
/// Helper method to execute a block on our serial queue (if it's enabled).
83-
public func sync<T>(execute work: () throws -> T) rethrows -> T {
84-
#if ENABLE_THREAD_SANITIZER
85-
return try _queue.blocking_sync { try work() }
86-
#else
87-
return try work()
88-
#endif
77+
_value.store(0, ordering: .relaxed)
8978
}
9079
}
9180

@@ -97,3 +86,44 @@ public let allStatistics = StatisticsGroup("swift-build")
9786
public func +=(statistic: Statistic, rhs: Int = 1) {
9887
statistic.increment(rhs)
9988
}
89+
90+
// MARK: Back-deployment
91+
92+
public final class Statistic: @unchecked Sendable, _StatisticBackend {
93+
public let name: String
94+
private let _statistic: (any _StatisticBackend)?
95+
96+
public init(_ name: String, _ description: String, _ group: StatisticsGroup = allStatistics) {
97+
self.name = name
98+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
99+
_statistic = _Statistic(name, description, group)
100+
} else {
101+
_statistic = nil
102+
}
103+
}
104+
105+
public var value: Int {
106+
_statistic?.value ?? 0
107+
}
108+
109+
public func increment(_ n: Int) {
110+
_statistic?.increment(n)
111+
}
112+
113+
public func zero() {
114+
_statistic?.zero()
115+
}
116+
}
117+
118+
public protocol _StatisticBackend {
119+
var name: String { get }
120+
var value: Int { get }
121+
func increment(_ n: Int)
122+
func zero()
123+
}
124+
125+
extension _StatisticBackend {
126+
public func increment() {
127+
self.increment(1)
128+
}
129+
}

0 commit comments

Comments
 (0)