Skip to content

Commit b541236

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 b541236

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

Sources/Metrics/Metrics.swift

Lines changed: 42 additions & 0 deletions
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,43 @@ 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 with a provided clock.
139+
///
140+
/// - Parameters:
141+
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
142+
/// - body: The closure to record the duration of.
143+
@inlinable
144+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
145+
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
146+
clock: Clock = .continuous,
147+
isolation: isolated (any Actor)? = #isolation,
148+
body: () async throws(Failure) -> sending Result
149+
) async throws(Failure) -> sending Result where Clock.Duration == Duration {
150+
let start = clock.now
151+
defer {
152+
self.record(duration: start.duration(to: clock.now))
153+
}
154+
return try await body()
155+
}
156+
#endif
115157
}

Tests/MetricsTests/MetricsTests.swift

Lines changed: 58 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,41 @@ class MetricsExtensionsTests: XCTestCase {
220221
"expected value to match"
221222
)
222223
}
224+
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+
}
223259
}
224260

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

0 commit comments

Comments
 (0)