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/Sources/CoreMetrics/Locks.swift b/Sources/CoreMetrics/Locks.swift index 222170c..228b294 100644 --- a/Sources/CoreMetrics/Locks.swift +++ b/Sources/CoreMetrics/Locks.swift @@ -26,9 +26,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(WASILibc) -// No locking on WASILibc -#elseif canImport(Darwin) +#if canImport(Darwin) import Darwin #elseif os(Windows) import WinSDK @@ -38,6 +36,11 @@ import Glibc import Android #elseif canImport(Musl) import Musl +#elseif canImport(WASILibc) +import WASILibc +#if canImport(wasi_pthread) +import wasi_pthread +#endif #else #error("Unsupported runtime") #endif @@ -142,9 +145,7 @@ extension Lock: @unchecked Sendable {} /// 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) + #if os(Windows) fileprivate let rwlock: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) fileprivate var shared: Bool = true @@ -155,9 +156,7 @@ internal final class ReadWriteLock { /// Create a new lock. public init() { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) + #if os(Windows) InitializeSRWLock(self.rwlock) #else let err = pthread_rwlock_init(self.rwlock, nil) @@ -166,9 +165,7 @@ internal final class ReadWriteLock { } deinit { - #if canImport(WASILibc) - // WASILibc is single threaded, provides no locks - #elseif os(Windows) + #if os(Windows) // SRWLOCK does not need to be free'd #else let err = pthread_rwlock_destroy(self.rwlock) @@ -182,9 +179,7 @@ internal final class ReadWriteLock { /// 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) + #if os(Windows) AcquireSRWLockShared(self.rwlock) self.shared = true #else @@ -198,9 +193,7 @@ internal final class ReadWriteLock { /// 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) + #if os(Windows) AcquireSRWLockExclusive(self.rwlock) self.shared = false #else @@ -215,9 +208,7 @@ internal final class ReadWriteLock { /// 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 os(Windows) if self.shared { ReleaseSRWLockShared(self.rwlock) } else { 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. ///