Skip to content

Commit 3c0f419

Browse files
authored
Add Timer.measure methods (#140)
# 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
1 parent cbd39ce commit 3c0f419

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

Sources/Metrics/Metrics.swift

+40
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

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

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

0 commit comments

Comments
 (0)