diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index 0d309f9..a8ab6dc 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 @@ -74,3 +75,88 @@ extension Timer { } } } + +#if (os(macOS) && swift(>=5.7.1)) || (!os(macOS) && swift(>=5.7)) +extension Timer { + /// Convenience for recording a duration based on Duration. + /// + /// - parameters: + /// - duration: The duration to record. + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + public func record(duration: Duration) { + self.recordNanoseconds(duration.nanosecondsClamped) + } + + /// Convenience for recording a duration since Instant using provided Clock + /// + /// - parameters: + /// - instant: The instant to measure duration since + /// - clock: The clock to measure duration with + @inlinable + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + public func recordDurationSince( + instant: C.Instant, + clock: C = ContinuousClock.continuous + ) where C.Duration == Duration { + self.record(duration: instant.duration(to: clock.now)) + } +} + +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +internal extension Swift.Duration { + /// The duration represented as nanoseconds, clamped to maximum expressible value. + 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 + } +} + +extension Timer { + /// Convenience for measuring duration of a closure + /// + /// - parameters: + /// - body: Closure to run & record. + @inlinable + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + public func measure( + body: @escaping () throws -> T + ) rethrows -> T { + let start = ContinuousClock.continuous.now + defer { + self.recordDurationSince(instant: start, clock: ContinuousClock.continuous) + } + return try body() + } + + /// Convenience for measuring duration of an async closure with a provided clock + /// + /// - parameters: + /// - clock: The clock to measure closure duration with + /// - body: Closure to run & record. + @inlinable + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + public func measure( + clock: C = ContinuousClock.continuous, + body: @escaping () async throws -> T + ) async rethrows -> T where C.Duration == Duration { + let start = clock.now + defer { + self.recordDurationSince(instant: start, clock: clock) + } + return try await body() + } +} + +#endif // (os(macOS) && swift(>=5.7.1)) || (!os(macOS) && swift(>=5.7)) diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index 9c12bae..cd90dc9 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -152,6 +152,74 @@ class MetricsExtensionsTests: XCTestCase { testTimer.preferDisplayUnit(.days) XCTAssertEqual(testTimer.valueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match") } + + #if (os(macOS) && swift(>=5.7.1)) || (!os(macOS) && swift(>=5.7)) + func testTimerBlock() 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 testTimerWithDuration() 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: 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") + } + + func testTimerWithDurationOnContinuousClock() async 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 clock = ContinuousClock() + let start = clock.now + let duration = Duration.milliseconds(5) + try await Task.sleep(for: duration) + timer.recordDurationSince(instant: start, clock: clock) + + let expectedTimer = try metrics.expectTimer(name) + XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match") + XCTAssertGreaterThan(expectedTimer.values[0], duration.nanosecondsClamped, "expected delay to match") + } + + func testTimerWithDurationOnDefaultContinuousClock() async 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 start = ContinuousClock.now + let duration = Duration.milliseconds(5) + try await Task.sleep(for: duration) + timer.recordDurationSince(instant: start) + + let expectedTimer = try metrics.expectTimer(name) + XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match") + XCTAssertGreaterThan(expectedTimer.values[0], duration.nanosecondsClamped, "expected delay to match") + } + #endif } // https://bugs.swift.org/browse/SR-6310