Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds support for swift-concurrency Duration and async closures for Timer measurements #135

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Sources/Metrics/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<C: Clock>(
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<T>(
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<T, C: Clock>(
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))
68 changes: 68 additions & 0 deletions Tests/MetricsTests/MetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down