diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d852253..c2ae2b4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -39,3 +39,9 @@ jobs: release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main + + wasm-sdk: + name: WebAssembly Swift SDK + uses: apple/swift-nio/.github/workflows/wasm_swift_sdk.yml@main + with: + additional_command_arguments: "--target Metrics" diff --git a/Package.swift b/Package.swift index 2a7f4c6..fb3c1dc 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,8 @@ import PackageDescription +let swiftAtomics: PackageDescription.Target.Dependency = .product(name: "Atomics", package: "swift-atomics") + let package = Package( name: "swift-metrics", products: [ @@ -24,7 +26,8 @@ let package = Package( ], targets: [ .target( - name: "CoreMetrics" + name: "CoreMetrics", + dependencies: [swiftAtomics] ), .target( name: "Metrics", @@ -36,11 +39,21 @@ let package = Package( ), .testTarget( name: "MetricsTests", - dependencies: ["Metrics", "MetricsTestKit"] + dependencies: ["Metrics", "MetricsTestKit", swiftAtomics] ), ] ) +if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0") + ] +} else { + package.dependencies += [ + .package(path: "../swift-atomics") + ] +} + for target in package.targets { var settings = target.swiftSettings ?? [] settings.append(.enableExperimentalFeature("StrictConcurrency=complete")) diff --git a/Sources/CoreMetrics/Locks.swift b/Sources/CoreMetrics/Locks.swift deleted file mode 100644 index 222170c..0000000 --- a/Sources/CoreMetrics/Locks.swift +++ /dev/null @@ -1,281 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Metrics API open source project -// -// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(WASILibc) -// No locking on WASILibc -#elseif canImport(Darwin) -import Darwin -#elseif os(Windows) -import WinSDK -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Android) -import Android -#elseif canImport(Musl) -import Musl -#else -#error("Unsupported runtime") -#endif - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -internal final class Lock { - #if os(Windows) - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #else - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #endif - - /// Create a new lock. - public init() { - #if os(Windows) - InitializeSRWLock(self.mutex) - #else - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) - - let err = pthread_mutex_init(self.mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - deinit { - #if os(Windows) - // SRWLOCK does not need to be free'd - #else - let err = pthread_mutex_destroy(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - self.mutex.deallocate() - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - public func lock() { - #if os(Windows) - AcquireSRWLockExclusive(self.mutex) - #else - let err = pthread_mutex_lock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - public func unlock() { - #if os(Windows) - ReleaseSRWLockExclusive(self.mutex) - #else - let err = pthread_mutex_unlock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } -} - -extension Lock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() - } - - // specialise Void return (for performance) - @inlinable - func withLockVoid(_ body: () throws -> Void) rethrows { - try self.withLock(body) - } -} - -extension Lock: @unchecked Sendable {} - -/// A reader/writer threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_rwlock_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -internal final class ReadWriteLock { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - fileprivate let rwlock: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - fileprivate var shared: Bool = true - #else - fileprivate let rwlock: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #endif - - /// Create a new lock. - public init() { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - InitializeSRWLock(self.rwlock) - #else - let err = pthread_rwlock_init(self.rwlock, nil) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif - } - - deinit { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - // SRWLOCK does not need to be free'd - #else - let err = pthread_rwlock_destroy(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif - self.rwlock.deallocate() - } - - /// Acquire a reader lock. - /// - /// Whenever possible, consider using `withReaderLock` instead of this - /// method and `unlock`, to simplify lock handling. - public func lockRead() { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - AcquireSRWLockShared(self.rwlock) - self.shared = true - #else - let err = pthread_rwlock_rdlock(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif - } - - /// Acquire a writer lock. - /// - /// Whenever possible, consider using `withWriterLock` instead of this - /// method and `unlock`, to simplify lock handling. - public func lockWrite() { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - AcquireSRWLockExclusive(self.rwlock) - self.shared = false - #else - let err = pthread_rwlock_wrlock(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withReaderLock` and `withWriterLock` - /// instead of this method and `lockRead` and `lockWrite`, to simplify lock - /// handling. - public func unlock() { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) - if self.shared { - ReleaseSRWLockShared(self.rwlock) - } else { - ReleaseSRWLockExclusive(self.rwlock) - } - #else - let err = pthread_rwlock_unlock(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif - } -} - -extension ReadWriteLock { - /// Acquire the reader lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lockRead` and `unlock` - /// in most situations, as it ensures that the lock will be released - /// regardless of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the reader lock. - /// - Returns: The value returned by the block. - @inlinable - func withReaderLock(_ body: () throws -> T) rethrows -> T { - self.lockRead() - defer { - self.unlock() - } - return try body() - } - - /// Acquire the writer lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lockWrite` and `unlock` - /// in most situations, as it ensures that the lock will be released - /// regardless of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the writer lock. - /// - Returns: The value returned by the block. - @inlinable - func withWriterLock(_ body: () throws -> T) rethrows -> T { - self.lockWrite() - defer { - self.unlock() - } - return try body() - } - - // specialise Void return (for performance) - @inlinable - func withReaderLockVoid(_ body: () throws -> Void) rethrows { - try self.withReaderLock(body) - } - - // specialise Void return (for performance) - @inlinable - func withWriterLockVoid(_ body: () throws -> Void) rethrows { - try self.withWriterLock(body) - } -} - -extension ReadWriteLock: @unchecked Sendable {} diff --git a/Sources/CoreMetrics/Metrics.swift b/Sources/CoreMetrics/Metrics.swift index 84ce691..02e4fbb 100644 --- a/Sources/CoreMetrics/Metrics.swift +++ b/Sources/CoreMetrics/Metrics.swift @@ -12,6 +12,89 @@ // //===----------------------------------------------------------------------===// +import Atomics + +// MARK: - Atomic helpers + +@usableFromInline +internal struct AtomicDouble: Sendable { + @usableFromInline + internal let storage: ManagedAtomic + + @inlinable + internal init(_ initialValue: Double) { + self.storage = ManagedAtomic(initialValue.bitPattern) + } + + @_semantics("atomics.requires_constant_orderings") + @_transparent @_alwaysEmitIntoClient + @inlinable + internal func load(ordering: AtomicLoadOrdering = AtomicLoadOrdering.acquiring) -> Double { + Double(bitPattern: self.storage.load(ordering: ordering)) + } + + @_semantics("atomics.requires_constant_orderings") + @_transparent @_alwaysEmitIntoClient + @inlinable + internal func store(_ value: Double, ordering: AtomicStoreOrdering = .releasing) { + self.storage.store(value.bitPattern, ordering: ordering) + } + + @_semantics("atomics.requires_constant_orderings") + @_transparent @_alwaysEmitIntoClient + @inlinable + internal func compareExchange( + expected: inout Double, + desired: Double, + ordering: AtomicUpdateOrdering = .acquiringAndReleasing + ) -> Bool { + let expectedBits = expected.bitPattern + let result = self.storage.compareExchange( + expected: expectedBits, + desired: desired.bitPattern, + ordering: ordering + ) + expected = Double(bitPattern: result.original) + return result.exchanged + } +} + +@usableFromInline +internal struct AtomicSpinLock: Sendable { + // 0 = unlocked, 1 = locked + @usableFromInline + internal let state = ManagedAtomic(0) + + @inlinable + internal init() {} + + @inlinable + internal func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try body() + } + + @inlinable + internal func lock() { + while true { + let attempt = self.state.compareExchange( + expected: 0, + desired: 1, + ordering: .acquiringAndReleasing + ) + if attempt.exchanged { return } + // Busy-wait + _ = self.state.load(ordering: .relaxed) + } + } + + @inlinable + internal func unlock() { + self.state.store(0, ordering: .releasing) + } +} + // MARK: - User API // MARK: - Counter @@ -858,12 +941,37 @@ extension Timer: CustomStringConvertible { // MARK: - MetricsSystem +//private final class _MetricsSystemState: AtomicReference { +// let factory: MetricsFactory +// let initialized: Bool +// +// init(factory: MetricsFactory, initialized: Bool) { +// self.factory = factory +// self.initialized = initialized +// } +//} +private final class _MetricsSystemState: AtomicReference, Sendable { + let factory: MetricsFactory + let initialized: Bool + + init(factory: MetricsFactory, initialized: Bool) { + self.factory = factory + self.initialized = initialized + } +} + /// A global facility where the default metrics backend implementation is configured. /// /// `MetricsSystem` is set up just once in a given program to create the desired metrics backend /// implementation using ``MetricsFactory``. public enum MetricsSystem { - private static let _factory = FactoryBox(NOOPMetricsHandler.instance) + private static let _state = ManagedAtomic<_MetricsSystemState>( + _MetricsSystemState(factory: NOOPMetricsHandler.instance, initialized: false) + ) + + // Keep a real exclusion primitive for callers that used this as global serialization. + // (No pthreads; simple atomic spin lock.) + private static let _writerLock = AtomicSpinLock() /// A one-time configuration function which globally selects the desired metrics backend /// implementation. @@ -874,17 +982,17 @@ public enum MetricsSystem { /// - parameters: /// - factory: A factory that given an identifier produces instances of metrics handlers such as ``CounterHandler``, ``RecorderHandler``, or ``TimerHandler``. public static func bootstrap(_ factory: MetricsFactory) { - self._factory.replaceFactory(factory, validate: true) + self._replaceFactory(factory, validate: true) } // for our testing we want to allow multiple bootstrapping internal static func bootstrapInternal(_ factory: MetricsFactory) { - self._factory.replaceFactory(factory, validate: false) + self._replaceFactory(factory, validate: false) } /// Returns a reference to the configured factory. public static var factory: MetricsFactory { - self._factory.underlying + self._state.load(ordering: .acquiring).factory } /// Acquire a writer lock for the duration of the given block. @@ -892,38 +1000,21 @@ public enum MetricsSystem { /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. public static func withWriterLock(_ body: () throws -> T) rethrows -> T { - try self._factory.withWriterLock(body) + try self._writerLock.withLock(body) } - // This can be `@unchecked Sendable` because we're manually gating access to mutable state with a lock. - private final class FactoryBox: @unchecked Sendable { - private let lock = ReadWriteLock() - fileprivate var _underlying: MetricsFactory - private var initialized = false + private static func _replaceFactory(_ factory: MetricsFactory, validate: Bool) { + // Serialize factory replacement with the public writer lock to preserve previous semantics. + self._writerLock.withLock { + let current = self._state.load(ordering: .acquiring) - init(_ underlying: MetricsFactory) { - self._underlying = underlying - } + precondition( + !validate || !current.initialized, + "metrics system can only be initialized once per process. currently used factory: \(current.factory)" + ) - func replaceFactory(_ factory: MetricsFactory, validate: Bool) { - self.lock.withWriterLock { - precondition( - !validate || !self.initialized, - "metrics system can only be initialized once per process. currently used factory: \(self._underlying)" - ) - self._underlying = factory - self.initialized = true - } - } - - var underlying: MetricsFactory { - self.lock.withReaderLock { - self._underlying - } - } - - func withWriterLock(_ body: () throws -> T) rethrows -> T { - try self.lock.withWriterLock(body) + let next = _MetricsSystemState(factory: factory, initialized: true) + self._state.store(next, ordering: .sequentiallyConsistent) } } } @@ -1037,10 +1128,9 @@ public protocol MetricsFactory: _SwiftMetricsSendableProtocol { /// Wraps a CounterHandler, adding support for incrementing by floating point values by storing an accumulated floating point value and recording increments to the underlying CounterHandler after crossing integer boundaries. internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler { - private let lock = Lock() private let counterHandler: CounterHandler private let factory: MetricsFactory - internal var fraction: Double = 0 + internal let fraction = AtomicDouble(0) init(label: String, dimensions: [(String, String)], factory: MetricsFactory) { self.counterHandler = factory.makeCounter(label: label, dimensions: dimensions) @@ -1060,24 +1150,29 @@ internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCoun if amount.exponent >= 63 { // If amount is in Int64.max..<+Inf, ceil to Int64.max - self.lock.withLockVoid { - self.counterHandler.increment(by: .max) - } - } else { - // Split amount into integer and fraction components - var (increment, fraction) = self.integerAndFractionComponents(of: amount) - self.lock.withLockVoid { - // Add the fractional component to the accumulated fraction. - self.fraction += fraction - // self.fraction may have cross an integer boundary, Split it - // and add any integer component. - let (integer, fraction) = integerAndFractionComponents(of: self.fraction) - increment += integer - self.fraction = fraction - // Increment the handler by the total integer component. - if increment > 0 { - self.counterHandler.increment(by: increment) + self.counterHandler.increment(by: .max) + return + } + + // Split amount into integer and fraction components + let (baseIncrement, addFraction) = self.integerAndFractionComponents(of: amount) + + while true { + let current = self.fraction.load(ordering: .acquiring) + let combined = current + addFraction + let (carry, newFraction) = self.integerAndFractionComponents(of: combined) + let totalIncrement = baseIncrement + carry + + var expected = current + if self.fraction.compareExchange( + expected: &expected, + desired: newFraction, + ordering: .acquiringAndReleasing + ) { + if totalIncrement > 0 { + self.counterHandler.increment(by: totalIncrement) } + return } } } @@ -1090,10 +1185,8 @@ internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCoun } func reset() { - self.lock.withLockVoid { - self.fraction = 0 - self.counterHandler.reset() - } + self.fraction.store(0, ordering: .releasing) + self.counterHandler.reset() } func destroy() { @@ -1105,9 +1198,7 @@ internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCoun /// - Note: we can annotate this class as `@unchecked Sendable` because we are manually gating access to mutable state (i.e., the `value` property) via a Lock. internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable { private let recorderHandler: RecorderHandler - // FIXME: use swift-atomics when floating point support is available - private var value: Double = 0 - private let lock = Lock() + private let value = AtomicDouble(0) private let factory: MetricsFactory init(label: String, dimensions: [(String, String)], factory: MetricsFactory) { @@ -1126,62 +1217,49 @@ internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable { func increment(by amount: Double) { // Drop illegal values // - cannot increment by NaN - guard !amount.isNaN else { - return - } + guard !amount.isNaN else { return } // - cannot increment by infinite quantities - guard !amount.isInfinite else { - return - } + guard !amount.isInfinite else { return } // - cannot increment by negative values - guard amount.sign == .plus else { - return - } + guard amount.sign == .plus else { return } // - cannot increment by zero - guard !amount.isZero else { - return - } + guard !amount.isZero else { return } - let newValue: Double = self.lock.withLock { - self.value += amount - return self.value - } + let newValue = self.add(amount) self.recorderHandler.record(newValue) } func decrement(by amount: Double) { // Drop illegal values // - cannot decrement by NaN - guard !amount.isNaN else { - return - } + guard !amount.isNaN else { return } // - cannot decrement by infinite quantities - guard !amount.isInfinite else { - return - } + guard !amount.isInfinite else { return } // - cannot decrement by negative values - guard amount.sign == .plus else { - return - } + guard amount.sign == .plus else { return } // - cannot decrement by zero - guard !amount.isZero else { - return - } + guard !amount.isZero else { return } - let newValue: Double = self.lock.withLock { - self.value -= amount - return self.value - } + let newValue = self.add(-amount) self.recorderHandler.record(newValue) } private func _set(_ value: Double) { - self.lock.withLockVoid { - self.value = value - } + self.value.store(value, ordering: .releasing) self.recorderHandler.record(value) } + private func add(_ delta: Double) -> Double { + while true { + let current = self.value.load(ordering: .acquiring) + let next = current + delta + var expected = current + if self.value.compareExchange(expected: &expected, desired: next, ordering: .acquiringAndReleasing) { + return next + } + } + } + func destroy() { self.factory.destroyRecorder(self.recorderHandler) } @@ -1355,7 +1433,6 @@ public protocol MeterHandler: AnyObject, _SwiftMetricsSendableProtocol { /// /// - The `TimerHandler` must be a `class`. public protocol TimerHandler: AnyObject, _SwiftMetricsSendableProtocol { - /// Record a duration in nanoseconds. /// /// - parameters: diff --git a/Sources/Metrics/DispatchAsyncWasm/DispatchTime.swift b/Sources/Metrics/DispatchAsyncWasm/DispatchTime.swift new file mode 100644 index 0000000..20f18d9 --- /dev/null +++ b/Sources/Metrics/DispatchAsyncWasm/DispatchTime.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Metrics API open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// # About DispatchAsync +/// +/// DispatchAsync is a temporary experimental repository aimed at implementing missing Dispatch support in the Swift for WebAssembly SDK. +/// Currently, [Swift for WebAsssembly doesn't include Dispatch](https://book.swiftwasm.org/getting-started/porting.html#swift-foundation-and-dispatch) +/// But, Swift for WebAssembly does support Swift Concurrency. DispatchAsync implements a number of common Dispatch API's using Swift Concurrency +/// under the hood. +/// +/// The code in this folder is copy-paste-adapted from [swift-dispatch-async](https://github.com/PassiveLogic/swift-dispatch-async) +/// +/// Notes +/// - Copying here avoids adding a temporary new dependency on a repo that will eventually move into the Swift for WebAssembly SDK itself. +/// - This is a temporary measure to enable wasm compilation until swift-dispatch-async is adopted into the Swift for WebAssembly SDK. + +#if os(WASI) && !canImport(Dispatch) + +/// Drop-in replacement for ``Dispatch.DispatchTime``, implemented using pure Swift. +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +public struct DispatchTime { + private let instant: ContinuousClock.Instant + + /// The very first time someone intializes a DispatchTime instance, we + /// reference this static let, causing it to be initialized. + /// + /// This is the closest we can get to snapshotting the start time of the running + /// executable, without using OS-specific calls. We want + /// to avoid OS-specific calls to maximize portability. + /// + /// To keep this robust, we initialize `self.durationSinceBeginning` + /// to this value using a default value, which is guaranteed to run before any + /// initializers run. This guarantees that uptimeBeginning will be the very + /// first + @available(macOS 13, *) + private static let uptimeBeginning: ContinuousClock.Instant = ContinuousClock.Instant.now + + /// See documentation for ``uptimeBeginning``. We intentionally + /// use this to guarantee a capture of `now` to `uptimeBeginning` BEFORE + /// any DispatchTime instances are initialized. + private let durationSinceUptime = uptimeBeginning.duration(to: ContinuousClock.Instant.now) + + public init() { + self.instant = ContinuousClock.Instant.now + } + + public static func now() -> Self { + DispatchTime() + } + + public var uptimeNanoseconds: UInt64 { + let beginning = DispatchTime.uptimeBeginning + let uptimeDuration: Int64 = beginning.duration(to: self.instant).nanosecondsClamped + guard uptimeDuration >= 0 else { + assertionFailure("It shouldn't be possible to get a negative duration since uptimeBeginning.") + return 0 + } + return UInt64(uptimeDuration) + } +} + +// NOTE: The following was copied from swift-nio/Source/NIOCore/TimeAmount+Duration on June 27, 2025. +// +// See https://github.com/apple/swift-nio/blob/83bc5b58440373a7678b56fa0d9cc22ca55297ee/Sources/NIOCore/TimeAmount%2BDuration.swift +// +// It was copied rather than brought via dependencies to avoid introducing +// a dependency on swift-nio for such a small piece of code. +// +// This library will need to have no depedendencies to be able to be integrated into GCD. +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +extension Swift.Duration { + /// The duration represented as nanoseconds, clamped to maximum expressible value. + fileprivate var nanosecondsClamped: Int64 { + let components = self.components + + let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000) + let attosCompononentNanos = components.attoseconds / 1_000_000_000 + let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos) + + guard + !secondsComponentNanos.overflow, + !combinedNanos.overflow + else { + return .max + } + + return combinedNanos.partialValue + } +} + +#endif // #if os(WASI) && !canImport(Dispatch) diff --git a/Sources/Metrics/DispatchAsyncWasm/DispatchTimeInterval.swift b/Sources/Metrics/DispatchAsyncWasm/DispatchTimeInterval.swift new file mode 100644 index 0000000..fb40075 --- /dev/null +++ b/Sources/Metrics/DispatchAsyncWasm/DispatchTimeInterval.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Metrics API open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// # About DispatchAsync +/// +/// DispatchAsync is a temporary experimental repository aimed at implementing missing Dispatch support in the Swift for WebAssembly SDK. +/// Currently, [Swift for WebAssembly doesn't include Dispatch](https://book.swiftwasm.org/getting-started/porting.html#swift-foundation-and-dispatch) +/// But, Swift for WebAssembly does support Swift Concurrency. DispatchAsync implements a number of common Dispatch API's using Swift Concurrency +/// under the hood. +/// +/// The code in this folder is copy-paste-adapted from [swift-dispatch-async](https://github.com/PassiveLogic/swift-dispatch-async) +/// +/// Notes +/// - Copying here avoids adding a temporary new dependency on a repo that will eventually move into the Swift for WebAssembly SDK itself. +/// - This is a temporary measure to enable wasm compilation until swift-dispatch-async is adopted into the Swift for WebAssembly SDK. +/// - The code is completely elided except for wasm compilation targets. +/// - Only the minimum code needed for compilation is copied. + +#if os(WASI) && !canImport(Dispatch) + +private let kNanosecondsPerSecond: UInt64 = 1_000_000_000 +private let kNanosecondsPerMillisecond: UInt64 = 1_000_000 +private let kNanoSecondsPerMicrosecond: UInt64 = 1_000 + +/// NOTE: This is an excerpt from libDispatch, see +/// https://github.com/swiftlang/swift-corelibs-libdispatch/blob/main/src/swift/Time.swift#L168 +/// +/// Represents a time interval that can be used as an offset from a `DispatchTime` +/// or `DispatchWallTime`. +/// +/// For example: +/// let inOneSecond = DispatchTime.now() + DispatchTimeInterval.seconds(1) +/// +/// If the requested time interval is larger then the internal representation +/// permits, the result of adding it to a `DispatchTime` or `DispatchWallTime` +/// is `DispatchTime.distantFuture` and `DispatchWallTime.distantFuture` +/// respectively. Such time intervals compare as equal: +/// +/// let t1 = DispatchTimeInterval.seconds(Int.max) +/// let t2 = DispatchTimeInterval.milliseconds(Int.max) +/// let result = t1 == t2 // true +public enum DispatchTimeInterval: Equatable, Sendable { + case seconds(Int) + case milliseconds(Int) + case microseconds(Int) + case nanoseconds(Int) + case never + + internal var rawValue: Int64 { + switch self { + case .seconds(let s): return clampedInt64Product(Int64(s), Int64(kNanosecondsPerSecond)) + case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(kNanosecondsPerMillisecond)) + case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(kNanoSecondsPerMicrosecond)) + case .nanoseconds(let ns): return Int64(ns) + case .never: return Int64.max + } + } + + public static func == (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { + switch (lhs, rhs) { + case (.never, .never): return true + case (.never, _): return false + case (_, .never): return false + default: return lhs.rawValue == rhs.rawValue + } + } + + // Returns m1 * m2, clamped to the range [Int64.min, Int64.max]. + // Because of the way this function is used, we can always assume + // that m2 > 0. + private func clampedInt64Product(_ m1: Int64, _ m2: Int64) -> Int64 { + assert(m2 > 0, "multiplier must be positive") + let (result, overflow) = m1.multipliedReportingOverflow(by: m2) + if overflow { + return m1 > 0 ? Int64.max : Int64.min + } + return result + } +} + +#endif // #if os(WASI) && !canImport(Dispatch) diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index 4084729..7f7594c 100644 --- a/Sources/Metrics/Metrics.swift +++ b/Sources/Metrics/Metrics.swift @@ -16,10 +16,14 @@ // https://github.com/swiftlang/swift/issues/79285 @_exported import CoreMetrics -import Foundation +import typealias Foundation.TimeInterval @_exported import class CoreMetrics.Timer +#if canImport(Dispatch) +import Dispatch +#endif + extension Timer { /// Convenience for measuring duration of a closure. /// diff --git a/Tests/MetricsTests/CoreMetricsTests.swift b/Tests/MetricsTests/CoreMetricsTests.swift index 18432b0..caf2adc 100644 --- a/Tests/MetricsTests/CoreMetricsTests.swift +++ b/Tests/MetricsTests/CoreMetricsTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import Atomics import Foundation import MetricsTestKit import Testing @@ -129,7 +130,7 @@ struct MetricsTests { var values = counter.values #expect(values.count == 1, "expected number of entries to match") #expect(values == [2], "expected entries to match") - #expect(rawFpCounter.fraction == 0.25, "") + #expect(rawFpCounter.fraction.load(ordering: .acquiring) == 0.25, "") // Increment by a large value that should leave a fraction in the accumulator // 1110506744053.76 @@ -137,7 +138,10 @@ struct MetricsTests { values = counter.values #expect(values.count == 2, "expected number of entries to match") #expect(values == [2, 1_110_506_744_054], "expected entries to match") - #expect(rawFpCounter.fraction == 0.010009765625, "expected fractional accumulated value") + #expect( + rawFpCounter.fraction.load(ordering: .acquiring) == 0.010009765625, + "expected fractional accumulated value" + ) } @Test func recorders() throws { diff --git a/Tests/MetricsTests/TestSendable.swift b/Tests/MetricsTests/TestSendable.swift index 59f57a3..9fcc9fc 100644 --- a/Tests/MetricsTests/TestSendable.swift +++ b/Tests/MetricsTests/TestSendable.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import Atomics import Dispatch import Foundation import MetricsTestKit @@ -47,7 +48,7 @@ struct SendableTest { let task = Task.detached { () -> Double in counter.increment(by: value) let handler = counter._handler as! AccumulatingRoundingFloatingPointCounter - return handler.fraction + return handler.fraction.load(ordering: .acquiring) } let fraction = await task.value #expect(fraction == value)