diff --git a/Demo/UIHangDetectorDemo/AppDelegate.swift b/Demo/UIHangDetectorDemo/AppDelegate.swift index 9099434..f5628bc 100644 --- a/Demo/UIHangDetectorDemo/AppDelegate.swift +++ b/Demo/UIHangDetectorDemo/AppDelegate.swift @@ -7,8 +7,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private let hangDetector = UIHangDetector( - warningCriteria: 500(.milliseconds), - criticalCriteria: 1(.seconds), + warningCriteria: 600(.milliseconds), + criticalCriteria: 1000(.milliseconds), healthSignalInterval: 500(.milliseconds), healthSignalCheckInterval: 100(.milliseconds) ) diff --git a/Sources/UIHangDetector/AnyPublisher+init.swift b/Sources/UIHangDetector/AnyPublisher+init.swift new file mode 100644 index 0000000..89b6b3e --- /dev/null +++ b/Sources/UIHangDetector/AnyPublisher+init.swift @@ -0,0 +1,10 @@ +import Foundation +import Combine + +internal extension AnyPublisher where Failure == Never { + init(_ output: Output) { + self = Optional.Publisher(output) + .compactMap { $0 } + .eraseToAnyPublisher() + } +} diff --git a/Sources/UIHangDetector/HealthChecker.swift b/Sources/UIHangDetector/RunLoopHealthChecker.swift similarity index 70% rename from Sources/UIHangDetector/HealthChecker.swift rename to Sources/UIHangDetector/RunLoopHealthChecker.swift index afcd390..93448dd 100644 --- a/Sources/UIHangDetector/HealthChecker.swift +++ b/Sources/UIHangDetector/RunLoopHealthChecker.swift @@ -1,15 +1,16 @@ import Foundation import Combine -internal final class HealthChecker { +internal final class RunLoopHealthChecker { private let healthSubject = PassthroughSubject() - private let healthSignalSubject = CurrentValueSubject(Date.distantPast) private var timerThread: Thread? private var subscription: AnyCancellable? + private let target: RunLoop private let warningCriteria: TimeInterval private let criticalCriteria: TimeInterval + private let healthSignalInterval: TimeInterval private let healthSignalCheckInterval: TimeInterval var healthStream: AnyPublisher { @@ -19,12 +20,16 @@ internal final class HealthChecker { } init( + target: RunLoop, warningCriteria: Duration, criticalCriteria: Duration, + healthSignalInterval: Duration, healthSignalCheckInterval: Duration ) { + self.target = target self.warningCriteria = warningCriteria.converted(to: .seconds).value self.criticalCriteria = criticalCriteria.converted(to: .seconds).value + self.healthSignalInterval = healthSignalInterval.converted(to: .seconds).value self.healthSignalCheckInterval = healthSignalCheckInterval.converted(to: .seconds).value } @@ -39,11 +44,18 @@ internal final class HealthChecker { } private func startImpl() { - self.subscription = Timer.publish(every: self.healthSignalCheckInterval, on: RunLoop.current, in: .common) + let healthSignalCheckTimer = Timer + .publish(every: self.healthSignalCheckInterval, on: RunLoop.current, in: .common) .autoconnect() - .combineLatest(self.healthSignalSubject.receive(on: RunLoop.current)) + let healthSignalStream = Timer + .publish(every: self.healthSignalInterval, on: self.target, in: .common) + .autoconnect() + .prepend(AnyPublisher(Date()).receive(on: self.target)) + .receive(on: RunLoop.current) + + self.subscription = healthSignalCheckTimer.combineLatest(healthSignalStream) .compactMap { (now: Date, lastSignal: Date) -> TimeInterval in - now.timeIntervalSince(lastSignal) + return now.timeIntervalSince(lastSignal) } .map { (timeDiff: TimeInterval) -> Health in switch (timeDiff) { @@ -69,8 +81,4 @@ internal final class HealthChecker { self.timerThread?.cancel() self.timerThread = nil } - - func acceptHealthSignal() { - self.healthSignalSubject.send(Date()) - } } diff --git a/Sources/UIHangDetector/UIHangDetector.swift b/Sources/UIHangDetector/UIHangDetector.swift index 76aaf35..d61a53e 100644 --- a/Sources/UIHangDetector/UIHangDetector.swift +++ b/Sources/UIHangDetector/UIHangDetector.swift @@ -2,8 +2,7 @@ import Foundation import Combine public final class UIHangDetector { - private let healthSignalInterval: TimeInterval - private let healthChecker: HealthChecker + private let healthChecker: RunLoopHealthChecker private var timer: Timer? public var healthStream: AnyPublisher { @@ -18,29 +17,20 @@ public final class UIHangDetector { healthSignalInterval: Duration = 0.5(.seconds), healthSignalCheckInterval: Duration = 0.1(.seconds) ) { - self.healthSignalInterval = healthSignalInterval.converted(to: .seconds).value - self.healthChecker = HealthChecker( + self.healthChecker = RunLoopHealthChecker( + target: RunLoop.main, warningCriteria: warningCriteria, criticalCriteria: criticalCriteria, + healthSignalInterval: healthSignalInterval, healthSignalCheckInterval: healthSignalCheckInterval ) } public func start() { self.healthChecker.start() - - DispatchQueue.main.async { - self.healthChecker.acceptHealthSignal() - self.timer = Timer.scheduledTimer(withTimeInterval: self.healthSignalInterval, repeats: true) { _ in - self.healthChecker.acceptHealthSignal() - } - } } public func stop() { self.healthChecker.stop() - - self.timer?.invalidate() - self.timer = nil } }