Skip to content
Open
Show file tree
Hide file tree
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
10 changes: 10 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 31 additions & 23 deletions Sources/CoreMetrics/Locks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<SRWLOCK> =
UnsafeMutablePointer.allocate(capacity: 1)
#else
Expand All @@ -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()
Expand All @@ -72,21 +76,26 @@ 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.
///
/// 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)
Expand All @@ -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)
Expand All @@ -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<T>(_ body: () throws -> T) rethrows -> T {
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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<T>(_ body: () throws -> T) rethrows -> T {
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockRead()
defer {
self.unlock()
Expand All @@ -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<T>(_ body: () throws -> T) rethrows -> T {
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockWrite()
defer {
self.unlock()
Expand All @@ -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 {}
26 changes: 15 additions & 11 deletions Sources/Metrics/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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`.
Expand Down
13 changes: 13 additions & 0 deletions Tests/MetricsTests/CoreMetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -422,6 +433,7 @@ class MetricsTests: XCTestCase {
}
}

#if canImport(Dispatch)
func testMeterIncrement() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Tests/MetricsTests/MetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -259,6 +269,7 @@ class MetricsExtensionsTests: XCTestCase {
#endif
}

#if canImport(Dispatch)
// https://bugs.swift.org/browse/SR-6310
extension DispatchTimeInterval {
func nano() -> Int {
Expand Down Expand Up @@ -288,6 +299,7 @@ extension DispatchTimeInterval {
}
}
}
#endif

#if swift(>=5.7)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
Expand Down
1 change: 0 additions & 1 deletion Tests/MetricsTests/TestSendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//

import Dispatch
import MetricsTestKit
import XCTest

Expand Down
Loading