Skip to content

Commit c17e8de

Browse files
jakepetroulesLuke Daley
authored and
Luke Daley
committed
Implement Statistic with atomics so it's thread safe
Prevents false positives in TSAN which obscure other real issues.
1 parent 58862d3 commit c17e8de

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)