Skip to content

Commit 8e02f31

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 9c0646a commit 8e02f31

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

Sources/Metrics/Metrics.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extension Timer {
2424
/// - dimensions: The dimensions for the Timer.
2525
/// - body: Closure to run & record.
2626
@inlinable
27+
@available(*, deprecated, message: "Please use non-static version on an already created Timer")
2728
public static func measure<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
2829
let timer = Timer(label: label, dimensions: dimensions)
2930
let start = DispatchTime.now().uptimeNanoseconds
@@ -98,5 +99,41 @@ extension Timer {
9899

99100
self.recordNanoseconds(nanoseconds.partialValue)
100101
}
102+
103+
/// Convenience for measuring duration of a closure.
104+
///
105+
/// - Parameters:
106+
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
107+
/// - body: The closure to record the duration of.
108+
@inlinable
109+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
110+
public func measure<Result, Clock: _Concurrency.Clock>(
111+
clock: Clock = .continuous,
112+
body: () throws -> Result
113+
) rethrows -> Result where Clock.Duration == Duration {
114+
let start = clock.now
115+
defer {
116+
self.record(start.duration(to: clock.now))
117+
}
118+
return try body()
119+
}
120+
121+
/// Convenience for measuring duration of a closure with a provided clock.
122+
///
123+
/// - Parameters:
124+
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
125+
/// - body: The closure to record the duration of.
126+
@inlinable
127+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
128+
public func measure<Result, Clock: _Concurrency.Clock>(
129+
clock: Clock = .continuous,
130+
body: () async throws -> Result
131+
) async rethrows -> Result where Clock.Duration == Duration {
132+
let start = clock.now
133+
defer {
134+
self.record(start.duration(to: clock.now))
135+
}
136+
return try await body()
137+
}
101138
}
102139
#endif

Tests/MetricsTests/MetricsTests.swift

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import MetricsTestKit
1818
import XCTest
1919

20-
class MetricsExtensionsTests: XCTestCase {
20+
final class MetricsExtensionsTests: XCTestCase {
21+
@available(*, deprecated)
2122
func testTimerBlock() throws {
2223
// bootstrap with our test metrics
2324
let metrics = TestMetrics()
@@ -184,6 +185,39 @@ class MetricsExtensionsTests: XCTestCase {
184185
testTimer.preferDisplayUnit(.days)
185186
XCTAssertEqual(testTimer.valueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match")
186187
}
188+
189+
#if swift(>=5.7)
190+
func testTimerMeasure() async throws {
191+
// bootstrap with our test metrics
192+
let metrics = TestMetrics()
193+
MetricsSystem.bootstrapInternal(metrics)
194+
// run the test
195+
let name = "timer-\(UUID().uuidString)"
196+
let delay = Duration.milliseconds(5)
197+
let timer = Timer(label: name)
198+
try await timer.measure {
199+
try await Task.sleep(for: delay)
200+
}
201+
let expectedTimer = try metrics.expectTimer(name)
202+
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
203+
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
204+
}
205+
206+
func testTimerRecordDuration() throws {
207+
// bootstrap with our test metrics
208+
let metrics = TestMetrics()
209+
MetricsSystem.bootstrapInternal(metrics)
210+
// run the test
211+
let name = "test-timer"
212+
let timer = Timer(label: name)
213+
let duration = Duration.milliseconds(5)
214+
timer.record(duration)
215+
216+
let expectedTimer = try metrics.expectTimer(name)
217+
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
218+
XCTAssertEqual(expectedTimer.values[0], duration.nanosecondsClamped, "expected delay to match")
219+
}
220+
#endif
187221
}
188222

189223
// https://bugs.swift.org/browse/SR-6310
@@ -203,3 +237,25 @@ extension DispatchTimeInterval {
203237
}
204238
}
205239
}
240+
241+
#if swift(>=5.7)
242+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
243+
extension Swift.Duration {
244+
fileprivate var nanosecondsClamped: Int64 {
245+
let components = self.components
246+
247+
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
248+
let attosCompononentNanos = components.attoseconds / 1_000_000_000
249+
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
250+
251+
guard
252+
!secondsComponentNanos.overflow,
253+
!combinedNanos.overflow
254+
else {
255+
return .max
256+
}
257+
258+
return combinedNanos.partialValue
259+
}
260+
}
261+
#endif

0 commit comments

Comments
 (0)