From d72927df48a60da9d6876e646b67d0e8bf93772e Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 27 Jun 2023 04:54:11 -0700 Subject: [PATCH] `TimingUtil`: added synchronous API (#2716) --- Sources/Misc/DateAndTime/TimingUtil.swift | 88 ++++++++++++++++++++++ Tests/UnitTests/Misc/TimingUtilTests.swift | 66 ++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/Sources/Misc/DateAndTime/TimingUtil.swift b/Sources/Misc/DateAndTime/TimingUtil.swift index d527496d71..bbf0a411f1 100644 --- a/Sources/Misc/DateAndTime/TimingUtil.swift +++ b/Sources/Misc/DateAndTime/TimingUtil.swift @@ -19,6 +19,8 @@ internal enum TimingUtil { } +// MARK: - async API + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) extension TimingUtil { @@ -105,6 +107,92 @@ extension TimingUtil { } +// MARK: - Synchronous API + +extension TimingUtil { + + /// Measures the time to execute `work` and returns the result and the duration. + /// Example: + /// ```swift + /// let (result, duration) = try TimingUtil.measure { + /// return try method() + /// } + /// ``` + static func measureSync( + _ work: () throws -> Value + ) rethrows -> (result: Value, duration: Duration) { + let start: DispatchTime = .now() + let result = try work() + + return ( + result: result, + duration: start.durationUntilNow + ) + } + + /// Measures the time to execute `work`, returns the result, + /// and logs `message` if duration exceeded `threshold`. + /// Example: + /// ```swift + /// let result = try TimingUtil.measureAndLogIfTooSlow( + /// threshold: .productRequest, + /// message: "Computation too slow", + /// level: .warn, + /// intent: .appleWarning + /// ) { + /// try method() + /// } + /// ``` + static func measureSyncAndLogIfTooSlow( + threshold: Configuration.TimingThreshold, + message: Message, + level: LogLevel = .warn, + intent: LogIntent = .appleWarning, + _ work: () throws -> Value + ) rethrows -> Value { + return try self.measureSyncAndLogIfTooSlow( + threshold: threshold.rawValue, + message: message, + level: level, + intent: intent, + work) + } + + /// Measures the time to execute `work`, returns the result, + /// and logs `message` if duration exceeded `threshold`. + /// Example: + /// ```swift + /// let result = try TimingUtil.measureAndLogIfTooSlow( + /// threshold: .productRequest, + /// message: "Computation too slow", + /// level: .warn, + /// intent: .appleWarning + /// ) { + /// try asyncMethod() + /// } + /// ``` + static func measureSyncAndLogIfTooSlow( + threshold: Duration, + message: Message, + level: LogLevel = .warn, + intent: LogIntent = .appleWarning, + _ work: () throws -> Value + ) rethrows -> Value { + let (result, duration) = try self.measureSync(work) + + Self.logIfRequired(duration: duration, + threshold: threshold, + message: message, + level: level, + intent: intent) + + return result + } + +} + +// MARK: - completion-block API + extension TimingUtil { /// Measures the time to execute `work` and returns the `Result` and the duration. diff --git a/Tests/UnitTests/Misc/TimingUtilTests.swift b/Tests/UnitTests/Misc/TimingUtilTests.swift index 0d1e8044af..e305025da7 100644 --- a/Tests/UnitTests/Misc/TimingUtilTests.swift +++ b/Tests/UnitTests/Misc/TimingUtilTests.swift @@ -121,6 +121,72 @@ class TimingUtilAsyncTests: TestCase { expect(logger.messages).to(beEmpty()) } + func testMeasureSyncAndLogDoesNotLogIfLowerThanThreshold() { + let logger = TestLogHandler() + + let expectedResult = Int.random(in: 0..<1000) + let threshold: DispatchTimeInterval = .milliseconds(10) + let sleepDuration = threshold + .milliseconds(-5) + + let result: Int = TimingUtil.measureSyncAndLogIfTooSlow(threshold: threshold.seconds, + message: "Too slow") { + Thread.sleep(forTimeInterval: sleepDuration.seconds) + + return expectedResult + } + + expect(result) == expectedResult + expect(logger.messages).to(beEmpty()) + } + + func testMeasureSyncAndLogThrowsError() { + let logger = TestLogHandler() + + let expectedError: ErrorCode = .storeProblemError + + do { + _ = try TimingUtil.measureSyncAndLogIfTooSlow(threshold: 0.001, + message: "Too slow") { + throw expectedError + } + + fail("Expected error") + } catch { + expect(error).to(matchError(expectedError)) + } + + expect(logger.messages).to(beEmpty()) + } + + func testMeasureSyncAndLogWithResult() { + let logger = TestLogHandler() + + let expectedResult = Int.random(in: 0..<1000) + let threshold: DispatchTimeInterval = .milliseconds(10) + let sleepDuration = threshold + .milliseconds(10) + + let message = "Computation took too long" + let level: LogLevel = .info + + let result = TimingUtil.measureSyncAndLogIfTooSlow(threshold: threshold.seconds, + message: message, + level: level) { () -> Int in + Thread.sleep(forTimeInterval: sleepDuration.seconds) + + return expectedResult + } + + expect(result) == expectedResult + + // Expected: 🍎⚠️ Computation took too long (0.02 seconds) + logger.verifyMessageWasLogged( + String(format: "%@ %@ (%.2f seconds)", + LogIntent.appleWarning.prefix, + message, + sleepDuration.seconds), + level: level + ) + } } class TimingUtilCompletionBlockTests: TestCase {