diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b2276f8..97bf449 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,3 +28,13 @@ jobs: release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main + + wasm-sdk: + name: WebAssembly Swift SDK + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_wasm_sdk_build: true + enable_linux_checks: false + enable_windows_checks: false + swift_flags: --target Metrics + swift_nightly_flags: --target Metrics diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0a5706c..7c8a4de 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -37,3 +37,13 @@ jobs: release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main + + wasm-sdk: + name: WebAssembly Swift SDK + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_wasm_sdk_build: true + enable_linux_checks: false + enable_windows_checks: false + swift_flags: --target Metrics + swift_nightly_flags: --target Metrics diff --git a/Sources/CoreMetrics/Locks.swift b/Sources/CoreMetrics/Locks.swift index 222170c..ff35f22 100644 --- a/Sources/CoreMetrics/Locks.swift +++ b/Sources/CoreMetrics/Locks.swift @@ -2,7 +2,7 @@ // // 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 +// Copyright (c) 2018-2025 Apple Inc. and the Swift Metrics API project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -48,8 +48,10 @@ import Musl /// 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) +internal final class Lock: @unchecked Sendable { + #if canImport(WASILibc) + // WASILibc is single threaded, provides no locks + #elseif os(Windows) fileprivate let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #else @@ -59,7 +61,9 @@ internal final class Lock { /// Create a new lock. public init() { - #if os(Windows) + #if canImport(WASILibc) + // WASILibc is single threaded, provides no locks + #elseif os(Windows) InitializeSRWLock(self.mutex) #else var attr = pthread_mutexattr_t() @@ -72,13 +76,16 @@ internal final class Lock { } deinit { - #if os(Windows) + #if canImport(WASILibc) + // WASILibc is single threaded, provides no locks + #elseif os(Windows) // SRWLOCK does not need to be free'd + self.mutex.deallocate() #else let err = pthread_mutex_destroy(self.mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif self.mutex.deallocate() + #endif } /// Acquire the lock. @@ -86,7 +93,9 @@ internal final class Lock { /// Whenever possible, consider using `withLock` instead of this method and /// `unlock`, to simplify lock handling. public func lock() { - #if os(Windows) + #if canImport(WASILibc) + // WASILibc is single threaded, provides no locks + #elseif os(Windows) AcquireSRWLockExclusive(self.mutex) #else let err = pthread_mutex_lock(self.mutex) @@ -99,7 +108,9 @@ internal final class Lock { /// Whenever possible, consider using `withLock` instead of this method and /// `lock`, to simplify lock handling. public func unlock() { - #if os(Windows) + #if canImport(WASILibc) + // WASILibc is single threaded, provides no locks + #elseif os(Windows) ReleaseSRWLockExclusive(self.mutex) #else let err = pthread_mutex_unlock(self.mutex) @@ -118,7 +129,7 @@ extension Lock { /// - 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 { + internal func withLock(_ body: () throws -> T) rethrows -> T { self.lock() defer { self.unlock() @@ -128,20 +139,18 @@ extension Lock { // specialise Void return (for performance) @inlinable - func withLockVoid(_ body: () throws -> Void) rethrows { + internal 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 { +internal final class ReadWriteLock: @unchecked Sendable { #if canImport(WASILibc) // WASILibc is single threaded, provides no locks #elseif os(Windows) @@ -170,18 +179,19 @@ internal final class ReadWriteLock { // WASILibc is single threaded, provides no locks #elseif os(Windows) // SRWLOCK does not need to be free'd + self.rwlock.deallocate() #else let err = pthread_rwlock_destroy(self.rwlock) precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - #endif self.rwlock.deallocate() + #endif } /// Acquire a reader lock. /// /// Whenever possible, consider using `withReaderLock` instead of this /// method and `unlock`, to simplify lock handling. - public func lockRead() { + fileprivate func lockRead() { #if canImport(WASILibc) // WASILibc is single threaded, provides no locks #elseif os(Windows) @@ -197,7 +207,7 @@ internal final class ReadWriteLock { /// /// Whenever possible, consider using `withWriterLock` instead of this /// method and `unlock`, to simplify lock handling. - public func lockWrite() { + fileprivate func lockWrite() { #if canImport(WASILibc) // WASILibc is single threaded, provides no locks #elseif os(Windows) @@ -214,7 +224,7 @@ internal final class ReadWriteLock { /// Whenever possible, consider using `withReaderLock` and `withWriterLock` /// instead of this method and `lockRead` and `lockWrite`, to simplify lock /// handling. - public func unlock() { + fileprivate func unlock() { #if canImport(WASILibc) // WASILibc is single threaded, provides no locks #elseif os(Windows) @@ -240,7 +250,7 @@ extension ReadWriteLock { /// - 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 { + internal func withReaderLock(_ body: () throws -> T) rethrows -> T { self.lockRead() defer { self.unlock() @@ -257,7 +267,7 @@ extension ReadWriteLock { /// - 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 { + internal func withWriterLock(_ body: () throws -> T) rethrows -> T { self.lockWrite() defer { self.unlock() @@ -267,15 +277,13 @@ extension ReadWriteLock { // specialise Void return (for performance) @inlinable - func withReaderLockVoid(_ body: () throws -> Void) rethrows { + internal func withReaderLockVoid(_ body: () throws -> Void) rethrows { try self.withReaderLock(body) } // specialise Void return (for performance) @inlinable - func withWriterLockVoid(_ body: () throws -> Void) rethrows { + internal func withWriterLockVoid(_ body: () throws -> Void) rethrows { try self.withWriterLock(body) } } - -extension ReadWriteLock: @unchecked Sendable {} diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index 963c296..88261f7 100644 --- a/Sources/Metrics/Metrics.swift +++ b/Sources/Metrics/Metrics.swift @@ -20,6 +20,9 @@ import Foundation @_exported import class CoreMetrics.Timer +#if canImport(Dispatch) +import Dispatch + extension Timer { /// Convenience for measuring duration of a closure. /// @@ -50,17 +53,6 @@ extension Timer { public func recordInterval(since: DispatchTime, end: DispatchTime = .now()) { self.recordNanoseconds(end.uptimeNanoseconds - since.uptimeNanoseconds) } -} - -extension Timer { - /// Convenience for recording a duration based on TimeInterval. - /// - /// - parameters: - /// - duration: The duration to record. - @inlinable - public func record(_ duration: TimeInterval) { - self.recordSeconds(duration) - } /// Convenience for recording a duration based on DispatchTimeInterval. /// @@ -94,6 +86,18 @@ extension Timer { } } } +#endif + +extension Timer { + /// Convenience for recording a duration based on TimeInterval. + /// + /// - parameters: + /// - duration: The duration to record. + @inlinable + public func record(_ duration: TimeInterval) { + self.recordSeconds(duration) + } +} extension Timer { /// Convenience for recording a duration based on `Duration`. diff --git a/Tests/MetricsTests/CoreMetricsTests.swift b/Tests/MetricsTests/CoreMetricsTests.swift index 7bc6c0b..c2ad62f 100644 --- a/Tests/MetricsTests/CoreMetricsTests.swift +++ b/Tests/MetricsTests/CoreMetricsTests.swift @@ -17,7 +17,13 @@ import XCTest @testable import CoreMetrics +#if canImport(Dispatch) +import Dispatch +#endif + class MetricsTests: XCTestCase { + + #if canImport(Dispatch) func testCounters() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -39,6 +45,7 @@ class MetricsTests: XCTestCase { testCounter.reset() XCTAssertEqual(testCounter.values.count, 0, "expected number of entries to match") } + #endif func testCounterBlock() throws { // bootstrap with our test metrics @@ -147,6 +154,7 @@ class MetricsTests: XCTestCase { XCTAssertEqual(rawFpCounter.fraction, 0.010009765625, "expected fractional accumulated value") } + #if canImport(Dispatch) func testRecorders() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -166,6 +174,7 @@ class MetricsTests: XCTestCase { group.wait() XCTAssertEqual(testRecorder.values.count, total, "expected number of entries to match") } + #endif func testRecordersInt() throws { // bootstrap with our test metrics @@ -212,6 +221,7 @@ class MetricsTests: XCTestCase { XCTAssertEqual(recorder.lastValue, value, "expected value to match") } + #if canImport(Dispatch) func testTimers() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -231,6 +241,7 @@ class MetricsTests: XCTestCase { group.wait() XCTAssertEqual(testTimer.values.count, total, "expected number of entries to match") } + #endif func testTimerBlock() throws { // bootstrap with our test metrics @@ -422,6 +433,7 @@ class MetricsTests: XCTestCase { } } + #if canImport(Dispatch) func testMeterIncrement() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -466,6 +478,7 @@ class MetricsTests: XCTestCase { XCTAssertEqual(testMeter.values.count, values.count, "expected number of entries to match") XCTAssertEqual(testMeter.values.last!, values.reduce(0.0, -), accuracy: 0.1, "expected total value to match") } + #endif func testDefaultMeterIgnoresNan() throws { // bootstrap with our test metrics diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index f554c72..08b4288 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -12,13 +12,20 @@ // //===----------------------------------------------------------------------===// +import Foundation import MetricsTestKit import XCTest @testable import CoreMetrics @testable import Metrics +#if canImport(Dispatch) +import Dispatch +#endif + class MetricsExtensionsTests: XCTestCase { + + #if canImport(Dispatch) func testTimerBlock() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -33,6 +40,7 @@ class MetricsExtensionsTests: XCTestCase { XCTAssertEqual(1, timer.values.count, "expected number of entries to match") XCTAssertGreaterThan(timer.values[0], Int64(delay * 1_000_000_000), "expected delay to match") } + #endif func testTimerWithTimeInterval() throws { // bootstrap with our test metrics @@ -47,6 +55,7 @@ class MetricsExtensionsTests: XCTestCase { XCTAssertEqual(testTimer.values[0], Int64(timeInterval * 1_000_000_000), "expected value to match") } + #if canImport(Dispatch) func testTimerWithDispatchTime() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -100,6 +109,7 @@ class MetricsExtensionsTests: XCTestCase { ) XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored") } + #endif func testTimerDuration() throws { // Wrapping only the insides of the test case so that the generated @@ -259,6 +269,7 @@ class MetricsExtensionsTests: XCTestCase { #endif } +#if canImport(Dispatch) // https://bugs.swift.org/browse/SR-6310 extension DispatchTimeInterval { func nano() -> Int { @@ -288,6 +299,7 @@ extension DispatchTimeInterval { } } } +#endif #if swift(>=5.7) @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) diff --git a/Tests/MetricsTests/TestSendable.swift b/Tests/MetricsTests/TestSendable.swift index 2cb09ba..41f28e2 100644 --- a/Tests/MetricsTests/TestSendable.swift +++ b/Tests/MetricsTests/TestSendable.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import Dispatch import MetricsTestKit import XCTest