Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UIHangeDetector #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Demo/UIHangDetectorDemo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
10 changes: 10 additions & 0 deletions Sources/UIHangDetector/AnyPublisher+init.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
import Combine

internal extension AnyPublisher where Failure == Never {
init(_ output: Output) {
self = Optional.Publisher(output)
.compactMap { $0 }
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Foundation
import Combine

internal final class HealthChecker {
internal final class RunLoopHealthChecker {
private let healthSubject = PassthroughSubject<Health, Never>()
private let healthSignalSubject = CurrentValueSubject<Date, Never>(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<Health, Never> {
Expand All @@ -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
}

Expand All @@ -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) {
Expand All @@ -69,8 +81,4 @@ internal final class HealthChecker {
self.timerThread?.cancel()
self.timerThread = nil
}

func acceptHealthSignal() {
self.healthSignalSubject.send(Date())
}
}
18 changes: 4 additions & 14 deletions Sources/UIHangDetector/UIHangDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Health, Never> {
Expand All @@ -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
}
}