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 Apr 22, 2024
1 parent 9c0646a commit 8e02f31
Showing 2 changed files with 94 additions and 1 deletion.
37 changes: 37 additions & 0 deletions Sources/Metrics/Metrics.swift
Original file line number Diff line number Diff line change
@@ -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<T>(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<Result, Clock: _Concurrency.Clock>(
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<Result, Clock: _Concurrency.Clock>(
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
58 changes: 57 additions & 1 deletion Tests/MetricsTests/MetricsTests.swift
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8e02f31

Please sign in to comment.