From ad702409de71a0eb8da3afb6b31855f4128958d8 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Thu, 1 May 2025 18:36:26 -0700 Subject: [PATCH] Implement Statistic with atomics so it's thread safe Prevents false positives in TSAN which obscure other real issues. --- Sources/SWBUtil/Statistics.swift | 74 ++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/Sources/SWBUtil/Statistics.swift b/Sources/SWBUtil/Statistics.swift index b7b38e1a..7f7eed87 100644 --- a/Sources/SWBUtil/Statistics.swift +++ b/Sources/SWBUtil/Statistics.swift @@ -10,21 +10,23 @@ // //===----------------------------------------------------------------------===// +import Synchronization + /// A scope to contain a group of statistics. public final class StatisticsGroup: Sendable { /// The name for this group. public let name: String - private let _statistics = LockedValue<[Statistic]>([]) + private let _statistics = LockedValue<[any _StatisticBackend]>([]) /// The list of statistics in the group. - public var statistics: [Statistic] { return _statistics.withLock { $0 } } + public var statistics: [any _StatisticBackend] { return _statistics.withLock { $0 } } public init(_ name: String) { self.name = name } - public func register(_ statistic: Statistic) { + public func register(_ statistic: any _StatisticBackend) { _statistics.withLock { $0.append(statistic) } } @@ -40,20 +42,16 @@ public final class StatisticsGroup: Sendable { /// /// 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. // FIXME: This should unconditionally be implemented using atomics, not conditionally be using a queue based on TSan... -public final class Statistic: @unchecked Sendable { +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2, *) +public final class _Statistic: @unchecked Sendable, _StatisticBackend { /// The name of the statistics. public let name: String /// The description of the statistics. public let description: String - #if ENABLE_THREAD_SANITIZER - /// Queue to serialize access to the statistic value. - let _queue = SWBQueue(label: "com.apple.dt.SWBUtil.Statistics") - #endif - /// The value of the statistic. - var _value: Int = 0 + private let _value = Atomic(0) public init(_ name: String, _ description: String, _ group: StatisticsGroup = allStatistics) { self.name = name @@ -64,28 +62,19 @@ public final class Statistic: @unchecked Sendable { /// Get the current value of the statistic. public var value: Int { - return sync { _value } + return _value.load(ordering: .relaxed) } /// Increment the statistic. public func increment(_ n: Int = 1) { - sync { _value += n } + _value.wrappingAdd(n, ordering: .relaxed) } /// Zero all of the statistics. /// /// This is useful when using statistics to probe program behavior from within tests, and the test can guarantee no concurrent access. public func zero() { - sync { _value = 0 } - } - - /// Helper method to execute a block on our serial queue (if it's enabled). - public func sync(execute work: () throws -> T) rethrows -> T { - #if ENABLE_THREAD_SANITIZER - return try _queue.blocking_sync { try work() } - #else - return try work() - #endif + _value.store(0, ordering: .relaxed) } } @@ -97,3 +86,44 @@ public let allStatistics = StatisticsGroup("swift-build") public func +=(statistic: Statistic, rhs: Int = 1) { statistic.increment(rhs) } + +// MARK: Back-deployment + +public final class Statistic: @unchecked Sendable, _StatisticBackend { + public let name: String + private let _statistic: (any _StatisticBackend)? + + public init(_ name: String, _ description: String, _ group: StatisticsGroup = allStatistics) { + self.name = name + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + _statistic = _Statistic(name, description, group) + } else { + _statistic = nil + } + } + + public var value: Int { + _statistic?.value ?? 0 + } + + public func increment(_ n: Int) { + _statistic?.increment(n) + } + + public func zero() { + _statistic?.zero() + } +} + +public protocol _StatisticBackend { + var name: String { get } + var value: Int { get } + func increment(_ n: Int) + func zero() +} + +extension _StatisticBackend { + public func increment() { + self.increment(1) + } +}