diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index 7a3ea82..6b0a378 100644 --- a/Sources/Metrics/Metrics.swift +++ b/Sources/Metrics/Metrics.swift @@ -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( + 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( + 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 } diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index 4319cac..f554c72 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -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