Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions Sources/SWBUtil/Statistics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}

Expand All @@ -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<Int>(0)

public init(_ name: String, _ description: String, _ group: StatisticsGroup = allStatistics) {
self.name = name
Expand All @@ -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<T>(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)
}
}

Expand All @@ -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)
}
}