Add `Timer.measure` methods

# Motivation

This PR supersedes https://github.com/apple/swift-metrics/pull/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.
This commit is contained in:
Franz Busch 2024-04-22 10:19:31 +01:00
parent cbd39ceaca
commit c91aab6cbe
2 changed files with 99 additions and 0 deletions

View File

@ -112,4 +112,44 @@ 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.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - isolation: The isolation of the method. Defaults to the isolation of the caller.
/// - 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
}

View File

@ -220,6 +220,43 @@ class MetricsExtensionsTests: XCTestCase {
"expected value to match"
)
}
#if compiler(>=6.0)
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")
}
#endif
}
// https://bugs.swift.org/browse/SR-6310
@ -251,3 +288,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