Skip to content

Commit 199b458

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 199b458

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

Sources/Metrics/Metrics.swift

+43
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ extension Timer {
2424
/// - label: The label for the Timer.
2525
/// - dimensions: The dimensions for the Timer.
2626
/// - body: Closure to run & record.
27+
#if compiler(>=6.0)
28+
@available(*, deprecated, message: "Please use non-static version on an already created Timer")
29+
#endif
2730
@inlinable
2831
public static func measure<T>(
2932
label: String,
@@ -112,4 +115,44 @@ extension Timer {
112115

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

Tests/MetricsTests/MetricsTests.swift

+60
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)