Skip to content

Commit 754f3a0

Browse files
authored
[Enhancement]Reduce the number of active timers (#854)
1 parent 6ea9106 commit 754f3a0

File tree

18 files changed

+225
-126
lines changed

18 files changed

+225
-126
lines changed

.swiftlint.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ included:
33
- Sources
44
- DemoApp
55
- DemoAppUIKit
6-
- StreamVideoTests
7-
- StreamVideoUIKitTests
86
excluded:
97
- Tests/SwiftLintFrameworkTests/Resources
108
- Sources/StreamVideo/Generated
119
- Sources/StreamVideo/OpenApi
1210
- Sources/StreamVideo/protobuf
1311
- Sources/StreamVideoSwiftUI/Generated
1412

13+
# Custom Rules
14+
custom_rules:
15+
discourage_timer_publish:
16+
name: "Discouraged Timer.publish usage"
17+
regex: '\bTimer\s*(\.\s*)?publish\b'
18+
message: "Avoid using Timer.publish. User the TimerStorage instead."
19+
severity: error
20+
1521
# Enabled/disabled rules
1622
only_rules:
17-
- unhandled_throwing_task
23+
- unhandled_throwing_task
24+
- custom_rules

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
1313

1414
@Injected(\.callCache) private var callCache
1515
@Injected(\.uuidFactory) private var uuidFactory
16+
@Injected(\.timers) private var timers
1617

1718
/// Represents a call that is being managed by the service.
1819
final class CallEntry: Equatable, @unchecked Sendable {
@@ -599,20 +600,16 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
599600
/// - Parameter callState: The state of the call.
600601
open func setUpRingingTimer(for callState: GetCallResponse) {
601602
let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
602-
ringingTimerCancellable = Foundation.Timer.publish(
603-
every: timeout,
604-
on: .main,
605-
in: .default
606-
)
607-
.autoconnect()
608-
.sink { [weak self] _ in
609-
log.debug(
610-
"Detected ringing timeout, hanging up...",
611-
subsystems: .callKit
612-
)
613-
self?.callEnded(callState.call.cid, ringingTimedOut: true)
614-
self?.ringingTimerCancellable = nil
615-
}
603+
ringingTimerCancellable = timers
604+
.timer(for: timeout)
605+
.sink { [weak self] _ in
606+
log.debug(
607+
"Detected ringing timeout, hanging up...",
608+
subsystems: .callKit
609+
)
610+
self?.callEnded(callState.call.cid, ringingTimedOut: true)
611+
self?.ringingTimerCancellable = nil
612+
}
616613
}
617614

618615
/// A method that's being called every time the StreamVideo instance is getting updated.

Sources/StreamVideo/CallState.swift

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public struct PermissionRequest: @unchecked Sendable, Identifiable {
3333
public class CallState: ObservableObject {
3434

3535
@Injected(\.streamVideo) var streamVideo
36-
36+
@Injected(\.timers) var timers
37+
3738
/// The id of the current session.
3839
/// When a call is started, a unique session identifier is assigned to the user in the call.
3940
@Published public internal(set) var sessionId: String = ""
@@ -153,7 +154,7 @@ public class CallState: ObservableObject {
153154
}
154155

155156
private var localCallSettingsUpdate = false
156-
private var durationTimer: Foundation.Timer?
157+
private var durationCancellable: AnyCancellable?
157158

158159
/// We mark this one as `nonisolated` to allow us to initialise a state instance without isolation.
159160
/// That's a safe operation because `MainActor` is only required to ensure that all `@Published`
@@ -497,36 +498,21 @@ public class CallState: ObservableObject {
497498

498499
private func setupDurationTimer() {
499500
resetTimer()
500-
durationTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] timer in
501-
guard let self else {
502-
timer.invalidate()
503-
return
504-
}
505-
Task {
506-
await MainActor.run {
507-
self.updateDuration()
501+
durationCancellable = timers
502+
.timer(for: 1.0)
503+
.receive(on: DispatchQueue.main)
504+
.compactMap { [weak self] _ in
505+
if let startedAt = self?.startedAt {
506+
return Date().timeIntervalSince(startedAt)
507+
} else {
508+
return 0
508509
}
509510
}
510-
})
511+
.assign(to: \.duration, onWeak: self)
511512
}
512513

513514
private func resetTimer() {
514-
durationTimer?.invalidate()
515-
durationTimer = nil
516-
}
517-
518-
@objc private func updateDuration() {
519-
guard let startedAt else {
520-
update(duration: 0)
521-
return
522-
}
523-
let timeInterval = Date().timeIntervalSince(startedAt)
524-
update(duration: timeInterval)
525-
}
526-
527-
private func update(duration: TimeInterval) {
528-
if duration != self.duration {
529-
self.duration = duration
530-
}
515+
durationCancellable?.cancel()
516+
durationCancellable = nil
531517
}
532518
}

Sources/StreamVideo/StreamVideo.swift

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public typealias UserTokenUpdater = @Sendable(UserToken) -> Void
1515
public class StreamVideo: ObservableObject, @unchecked Sendable {
1616

1717
@Injected(\.callCache) private var callCache
18+
@Injected(\.timers) private var timers
1819

1920
public final class State: ObservableObject, @unchecked Sendable {
2021
@Published public internal(set) var connection: ConnectionStatus
@@ -491,23 +492,16 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
491492
} else {
492493
throw ClientError.Unknown()
493494
}
494-
var connected = false
495-
var timeout = false
496-
let control = DefaultTimer.schedule(timeInterval: 30, queue: .sdk) {
497-
timeout = true
498-
}
495+
499496
log.debug("Listening for WS connection")
500-
webSocketClient?.onConnected = {
501-
control.cancel()
502-
connected = true
503-
log.debug("WS connected")
504-
}
505497

506-
while (!connected && !timeout) {
507-
try await Task.sleep(nanoseconds: 100_000)
508-
}
509-
510-
if timeout {
498+
do {
499+
log.debug("Listening for WS connection")
500+
_ = try await timers
501+
.timer(for: 0.1)
502+
.filter { [weak webSocketClient] _ in webSocketClient?.connectionState.isConnected == true }
503+
.nextValue(timeout: 30)
504+
} catch {
511505
log.debug("Timeout while waiting for WS connection opening")
512506
throw ClientError.NetworkError()
513507
}
@@ -557,18 +551,16 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
557551
}
558552
log.debug("Waiting for connection id")
559553

560-
while (loadConnectionIdFromHealthcheck() == nil && !timeout) {
561-
try? await Task.sleep(nanoseconds: 100_000)
562-
}
563-
564-
control.cancel()
565-
566-
if let connectionId = loadConnectionIdFromHealthcheck() {
567-
log.debug("Connection id available from the WS")
568-
return connectionId
554+
do {
555+
return try await timers
556+
.timer(for: 0.1)
557+
.log(.debug) { _ in "Waiting for connection id" }
558+
.compactMap { [weak self] _ in self?.loadConnectionIdFromHealthcheck() }
559+
.nextValue(timeout: 5)
560+
} catch {
561+
log.warning("Unable to load connectionId.")
562+
return ""
569563
}
570-
571-
return ""
572564
}
573565

574566
private func loadConnectionIdFromHealthcheck() -> String? {

Sources/StreamVideo/Utils/AudioSession/AudioRecorder/StreamCallAudioRecorder.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ open class StreamCallAudioRecorder: @unchecked Sendable {
1616

1717
@Injected(\.activeCallProvider) private var activeCallProvider
1818
@Injected(\.activeCallAudioSession) private var activeCallAudioSession
19+
@Injected(\.timers) private var timers
1920

2021
/// The builder used to create the AVAudioRecorder instance.
2122
let audioRecorderBuilder: AVAudioRecorderBuilder
@@ -123,10 +124,8 @@ open class StreamCallAudioRecorder: @unchecked Sendable {
123124

124125
updateMetersTimerCancellable?.cancel()
125126
disposableBag.remove("update-meters")
126-
updateMetersTimerCancellable = Foundation
127-
.Timer
128-
.publish(every: 0.1, on: .main, in: .default)
129-
.autoconnect()
127+
updateMetersTimerCancellable = timers
128+
.timer(for: ScreenPropertiesAdapter.currentValue.refreshRate)
130129
.sinkTask(storeIn: disposableBag, identifier: "update-meters") { [weak self, audioRecorder] _ in
131130
audioRecorder.updateMeters()
132131
self?._metersPublisher.send(audioRecorder.averagePower(forChannel: 0))

Sources/StreamVideo/Utils/OrderedCapacityQueue/OrderedCapacityQueue.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Foundation
88
/// A thread-safe queue that maintains a fixed capacity and removes elements after
99
/// a specified time interval.
1010
final class OrderedCapacityQueue<Element> {
11+
@Injected(\.timers) private var timers
12+
1113
private let queue = UnfairQueue()
1214

1315
/// The maximum number of elements the queue can hold.
@@ -48,10 +50,8 @@ final class OrderedCapacityQueue<Element> {
4850
init(capacity: Int, removalTime: TimeInterval) {
4951
self.capacity = capacity
5052
self.removalTime = removalTime
51-
removalTimerCancellable = Foundation
52-
.Timer
53-
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate, on: .main, in: .default)
54-
.autoconnect()
53+
removalTimerCancellable = timers
54+
.timer(for: ScreenPropertiesAdapter.currentValue.refreshRate)
5555
.sink { [weak self] _ in self?.removeItemsIfRequired() }
5656
}
5757

@@ -86,14 +86,8 @@ final class OrderedCapacityQueue<Element> {
8686
/// should be enabled.
8787
private func toggleRemovalObservation(_ isEnabled: Bool) {
8888
if isEnabled, removalTimerCancellable == nil {
89-
removalTimerCancellable = Foundation
90-
.Timer
91-
.publish(
92-
every: ScreenPropertiesAdapter.currentValue.refreshRate,
93-
on: .main,
94-
in: .default
95-
)
96-
.autoconnect()
89+
removalTimerCancellable = timers
90+
.timer(for: ScreenPropertiesAdapter.currentValue.refreshRate)
9791
.sink { [weak self] _ in self?.removeItemsIfRequired() }
9892
} else if !isEnabled, removalTimerCancellable != nil {
9993
removalTimerCancellable?.cancel()

Sources/StreamVideo/Utils/ScreenPropertiesAdapter/ScreenPropertiesAdapter.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import Foundation
77
import UIKit
88
#endif
99

10-
final class ScreenPropertiesAdapter: @unchecked Sendable {
10+
public final class ScreenPropertiesAdapter: @unchecked Sendable {
1111

12-
private(set) var preferredFramesPerSecond: Int = 0
13-
private(set) var refreshRate: TimeInterval = 0
14-
private(set) var scale: CGFloat = 0
12+
public private(set) var preferredFramesPerSecond: Int = 0
13+
public private(set) var refreshRate: TimeInterval = 0
14+
public private(set) var scale: CGFloat = 0
1515

1616
init() {
1717
Task { @MainActor in
@@ -29,11 +29,11 @@ final class ScreenPropertiesAdapter: @unchecked Sendable {
2929
}
3030

3131
extension ScreenPropertiesAdapter: InjectionKey {
32-
nonisolated(unsafe) static var currentValue: ScreenPropertiesAdapter = .init()
32+
public nonisolated(unsafe) static var currentValue: ScreenPropertiesAdapter = .init()
3333
}
3434

3535
extension InjectedValues {
36-
var screenProperties: ScreenPropertiesAdapter {
36+
public var screenProperties: ScreenPropertiesAdapter {
3737
set { Self[ScreenPropertiesAdapter.self] = newValue }
3838
get { Self[ScreenPropertiesAdapter.self] }
3939
}

0 commit comments

Comments
 (0)