Timer.record(_ duration:): protect from overflows

This commit is contained in:
Natik Gadzhi 2023-06-26 21:07:03 -07:00
parent b01946077d
commit 02fc732f99
No known key found for this signature in database
GPG Key ID: E6387D41B53DEE06
2 changed files with 30 additions and 9 deletions

View File

@ -76,20 +76,37 @@ extension Timer {
} }
#if swift(>=5.7) #if swift(>=5.7)
public enum TimerError: Error {
case durationToIntOverflow
}
extension Timer { extension Timer {
/// Convenience for recording a duration based on ``Duration``. /// Convenience for recording a duration based on ``Duration``.
/// Duration will be recorded in nanoseconds.
/// ///
/// - parameters: /// `Duration` will be converted to an `Int64` number of nanoseconds, and then recorded with nanosecond precision.
/// - duration: The duration to record. ///
/// - Parameters:
/// - duration: The `Duration` to record.
///
/// - Throws: `TimerError.durationToIntOverflow` if conversion from `Duration` to `Int64` of Nanoseconds overflowed.
@available(macOS 13, iOS 16, tvOS 15, watchOS 8, *) @available(macOS 13, iOS 16, tvOS 15, watchOS 8, *)
@inlinable @inlinable
public func record(_ duration: Duration) { public func record(_ duration: Duration) throws {
// `Duration` doesn't have a nice way to convert it nanoseconds or seconds, // `Duration` doesn't have a nice way to convert it nanoseconds or seconds,
// so we'll do the multiplication manually. // so we'll do the multiplication manually.
// nanoseconds are the smallest unit Timer can track, so we'll record in that. // Nanoseconds are the smallest unit Timer can track, so we'll record in that.
let durationNanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000 let (seconds, overflow) = duration.components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
self.recordNanoseconds(durationNanoseconds) guard !overflow else {
throw TimerError.durationToIntOverflow
}
let (nanoseconds, attosecondsOverflow) = seconds.addingReportingOverflow(duration.components.attoseconds / 1_000_000_000)
guard !attosecondsOverflow else {
throw TimerError.durationToIntOverflow
}
self.recordNanoseconds(nanoseconds)
} }
} }
#endif #endif

View File

@ -108,16 +108,20 @@ class MetricsExtensionsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics) MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)" let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000) let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000)
let durationInNanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000 let durationInNanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000
let timer = Timer(label: name) XCTAssertNoThrow(try timer.record(duration))
timer.record(duration)
let testTimer = try metrics.expectTimer(timer) let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first, durationInNanoseconds, "expected value to match") XCTAssertEqual(testTimer.values.first, durationInNanoseconds, "expected value to match")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored") XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
let overflowDuration = Duration(secondsComponent: 10_000_000_000, attosecondsComponent: 123)
XCTAssertThrowsError(try timer.record(overflowDuration))
#endif #endif
} }