diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index c916c76..81a05ae 100644 --- a/Sources/Metrics/Metrics.swift +++ b/Sources/Metrics/Metrics.swift @@ -24,6 +24,7 @@ extension Timer { /// - dimensions: The dimensions for the Timer. /// - body: Closure to run & record. @inlinable + @available(*, deprecated, message: "Please use non-static version on an already created Timer") public static func measure(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T { let timer = Timer(label: label, dimensions: dimensions) let start = DispatchTime.now().uptimeNanoseconds @@ -98,5 +99,41 @@ extension Timer { self.recordNanoseconds(nanoseconds.partialValue) } + + /// 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 -> Result + ) rethrows -> Result where Clock.Duration == Duration { + let start = clock.now + defer { + self.record(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( + clock: Clock = .continuous, + body: () async throws -> Result + ) async rethrows -> Result where Clock.Duration == Duration { + let start = clock.now + defer { + self.record(start.duration(to: clock.now)) + } + return try await body() + } } #endif diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index 3b590fd..d20a5f0 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -17,7 +17,8 @@ import MetricsTestKit import XCTest -class MetricsExtensionsTests: XCTestCase { +final class MetricsExtensionsTests: XCTestCase { + @available(*, deprecated) func testTimerBlock() throws { // bootstrap with our test metrics let metrics = TestMetrics() @@ -184,6 +185,39 @@ class MetricsExtensionsTests: XCTestCase { testTimer.preferDisplayUnit(.days) XCTAssertEqual(testTimer.valueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match") } + + #if swift(>=5.7) + 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") + } + + func testTimerRecordDuration() throws { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + // run the test + let name = "test-timer" + let timer = Timer(label: name) + let duration = Duration.milliseconds(5) + timer.record(duration) + + let expectedTimer = try metrics.expectTimer(name) + XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match") + XCTAssertEqual(expectedTimer.values[0], duration.nanosecondsClamped, "expected delay to match") + } + #endif } // https://bugs.swift.org/browse/SR-6310 @@ -203,3 +237,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