Skip to content

Commit fca8582

Browse files
committed
Add Timer.measure methods
# 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.
1 parent cbd39ce commit fca8582

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

Sources/Metrics/Metrics.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,44 @@ extension Timer {
112112

113113
self.recordNanoseconds(nanoseconds.partialValue)
114114
}
115+
116+
#if compiler(>=6.0)
117+
/// Convenience for measuring duration of a closure.
118+
///
119+
/// - Parameters:
120+
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
121+
/// - body: The closure to record the duration of.
122+
@inlinable
123+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
124+
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
125+
clock: Clock = .continuous,
126+
body: () throws(Failure) -> Result
127+
) throws(Failure) -> Result where Clock.Duration == Duration {
128+
let start = clock.now
129+
defer {
130+
self.record(duration: start.duration(to: clock.now))
131+
}
132+
return try body()
133+
}
134+
135+
/// Convenience for measuring duration of a closure.
136+
///
137+
/// - Parameters:
138+
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
139+
/// - isolation: The isolation of the method. Defaults to the isolation of the caller.
140+
/// - body: The closure to record the duration of.
141+
@inlinable
142+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
143+
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
144+
clock: Clock = .continuous,
145+
isolation: isolated (any Actor)? = #isolation,
146+
body: () async throws(Failure) -> sending Result
147+
) async throws(Failure) -> sending Result where Clock.Duration == Duration {
148+
let start = clock.now
149+
defer {
150+
self.record(duration: start.duration(to: clock.now))
151+
}
152+
return try await body()
153+
}
154+
#endif
115155
}

Tests/MetricsTests/MetricsTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import XCTest
1919
@testable import Metrics
2020

2121
class MetricsExtensionsTests: XCTestCase {
22+
@available(*, deprecated)
2223
func testTimerBlock() throws {
2324
// bootstrap with our test metrics
2425
let metrics = TestMetrics()
@@ -220,6 +221,43 @@ class MetricsExtensionsTests: XCTestCase {
220221
"expected value to match"
221222
)
222223
}
224+
225+
#if compiler(>=6.0)
226+
func testTimerMeasure() async throws {
227+
// bootstrap with our test metrics
228+
let metrics = TestMetrics()
229+
MetricsSystem.bootstrapInternal(metrics)
230+
// run the test
231+
let name = "timer-\(UUID().uuidString)"
232+
let delay = Duration.milliseconds(5)
233+
let timer = Timer(label: name)
234+
try await timer.measure {
235+
try await Task.sleep(for: delay)
236+
}
237+
238+
let expectedTimer = try metrics.expectTimer(name)
239+
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
240+
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
241+
}
242+
243+
@MainActor
244+
func testTimerMeasureFromMainActor() async throws {
245+
// bootstrap with our test metrics
246+
let metrics = TestMetrics()
247+
MetricsSystem.bootstrapInternal(metrics)
248+
// run the test
249+
let name = "timer-\(UUID().uuidString)"
250+
let delay = Duration.milliseconds(5)
251+
let timer = Timer(label: name)
252+
try await timer.measure {
253+
try await Task.sleep(for: delay)
254+
}
255+
256+
let expectedTimer = try metrics.expectTimer(name)
257+
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
258+
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
259+
}
260+
#endif
223261
}
224262

225263
// https://bugs.swift.org/browse/SR-6310
@@ -251,3 +289,25 @@ extension DispatchTimeInterval {
251289
}
252290
}
253291
}
292+
293+
#if swift(>=5.7)
294+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
295+
extension Swift.Duration {
296+
fileprivate var nanosecondsClamped: Int64 {
297+
let components = self.components
298+
299+
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
300+
let attosCompononentNanos = components.attoseconds / 1_000_000_000
301+
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
302+
303+
guard
304+
!secondsComponentNanos.overflow,
305+
!combinedNanos.overflow
306+
else {
307+
return .max
308+
}
309+
310+
return combinedNanos.partialValue
311+
}
312+
}
313+
#endif

0 commit comments

Comments
 (0)