Skip to content

Commit

Permalink
Add Timer.measure methods
Browse files Browse the repository at this point in the history
# Motivation

This PR supersedes #135. The goal is to make it easier to measure asynchronous code when using `Metrics`.

# Modification

This PR does:
- Deprecate the current static method for measuring synchronous code
- Add a new instance method to measure synchronous code
- Add a new instance method to measure asynchronous code

# Result

It is now easier to measure asynchronous code.
  • Loading branch information
FranzBusch committed Mar 8, 2025
1 parent cbd39ce commit b541236
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 0 deletions.
42 changes: 42 additions & 0 deletions Sources/Metrics/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ extension Timer {
/// - label: The label for the Timer.
/// - dimensions: The dimensions for the Timer.
/// - body: Closure to run & record.
#if compiler(>=6.0)
@available(*, deprecated, message: "Please use non-static version on an already created Timer")
#endif
@inlinable
public static func measure<T>(
label: String,
Expand Down Expand Up @@ -112,4 +115,43 @@ extension Timer {

self.recordNanoseconds(nanoseconds.partialValue)
}

#if compiler(>=6.0)
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
body: () throws(Failure) -> Result
) throws(Failure) -> Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try body()
}

/// Convenience for measuring duration of a closure with a provided clock.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
isolation: isolated (any Actor)? = #isolation,
body: () async throws(Failure) -> sending Result
) async throws(Failure) -> sending Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try await body()
}
#endif
}
58 changes: 58 additions & 0 deletions Tests/MetricsTests/MetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import XCTest
@testable import Metrics

class MetricsExtensionsTests: XCTestCase {
@available(*, deprecated)
func testTimerBlock() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
Expand Down Expand Up @@ -220,6 +221,41 @@ class MetricsExtensionsTests: XCTestCase {
"expected value to match"
)
}

func testTimerMeasure() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}

let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}

@MainActor
func testTimerMeasureFromMainActor() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}

let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
}

// https://bugs.swift.org/browse/SR-6310
Expand Down Expand Up @@ -251,3 +287,25 @@ extension DispatchTimeInterval {
}
}
}

#if swift(>=5.7)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
extension Swift.Duration {
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

0 comments on commit b541236

Please sign in to comment.