Skip to content

Commit 1831493

Browse files
authored
[Performance]Timers improvements (#877)
1 parent 9b6aad5 commit 1831493

30 files changed

+639
-227
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55
# Upcoming
66

77
### 🔄 Changed
8+
- Performance improvements around timers. [#877](https://github.com/GetStream/stream-video-swift/pull/877)
89

910
# [1.27.2](https://github.com/GetStream/stream-video-swift/releases/tag/1.27.2)
1011
_June 25, 2025_

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
612612
/// - Parameter callState: The state of the call.
613613
open func setUpRingingTimer(for callState: GetCallResponse) {
614614
let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
615-
ringingTimerCancellable = Foundation
616-
.Timer
617-
.publish(every: timeout, on: .main, in: .default)
618-
.autoconnect()
615+
ringingTimerCancellable = DefaultTimer
616+
.publish(every: timeout)
619617
.sink { [weak self] _ in
620618
log.debug(
621619
"Detected ringing timeout, hanging up...",

Sources/StreamVideo/CallState.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,8 @@ public class CallState: ObservableObject {
506506

507507
private func setupDurationTimer() {
508508
resetTimer()
509-
durationCancellable = Foundation
510-
.Timer
511-
.publish(every: 1.0, on: .main, in: .default)
512-
.autoconnect()
509+
durationCancellable = DefaultTimer
510+
.publish(every: 1.0)
513511
.receive(on: DispatchQueue.main)
514512
.compactMap { [weak self] _ in
515513
if let startedAt = self?.startedAt {

Sources/StreamVideo/StreamVideo.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -531,10 +531,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
531531
do {
532532
var cancellable: AnyCancellable?
533533
log.debug("Listening for WS connection")
534-
_ = try await Foundation
535-
.Timer
536-
.publish(every: 0.1, on: .main, in: .default)
537-
.autoconnect()
534+
_ = try await DefaultTimer
535+
.publish(every: 0.1)
538536
.filter { [weak webSocketClient] _ in webSocketClient?.connectionState.isConnected == true }
539537
.nextValue(timeout: 30) { cancellable = $0 }
540538
cancellable?.cancel()
@@ -586,10 +584,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
586584

587585
var cancellable: AnyCancellable?
588586
do {
589-
let result = try await Foundation
590-
.Timer
591-
.publish(every: 0.1, on: .main, in: .default)
592-
.autoconnect()
587+
let result = try await DefaultTimer
588+
.publish(every: 0.1)
593589
.log(.debug) { _ in "Waiting for connection id" }
594590
.compactMap { [weak self] _ in self?.loadConnectionIdFromHealthcheck() }
595591
.nextValue(timeout: 5) { cancellable = $0 }

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,8 @@ open class StreamCallAudioRecorder: @unchecked Sendable {
118118

119119
updateMetersTimerCancellable?.cancel()
120120
disposableBag.remove("update-meters")
121-
updateMetersTimerCancellable = Foundation
122-
.Timer
123-
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate, on: .main, in: .default)
124-
.autoconnect()
121+
updateMetersTimerCancellable = DefaultTimer
122+
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate)
125123
.sinkTask(storeIn: disposableBag, identifier: "update-meters") { [weak self, audioRecorder] _ in
126124
audioRecorder.updateMeters()
127125
self?._metersPublisher.send(audioRecorder.averagePower(forChannel: 0))

Sources/StreamVideo/Utils/OrderedCapacityQueue/OrderedCapacityQueue.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ final class OrderedCapacityQueue<Element> {
4949
init(capacity: Int, removalTime: TimeInterval) {
5050
self.capacity = capacity
5151
self.removalTime = removalTime
52-
removalTimerCancellable = Foundation
53-
.Timer
54-
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate, on: .main, in: .default)
55-
.autoconnect()
52+
removalTimerCancellable = DefaultTimer
53+
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate)
5654
.receive(on: DispatchQueue.global(qos: .userInteractive))
5755
.sink { [weak self] _ in self?.removeItemsIfRequired() }
5856
}
@@ -88,10 +86,8 @@ final class OrderedCapacityQueue<Element> {
8886
/// should be enabled.
8987
private func toggleRemovalObservation(_ isEnabled: Bool) {
9088
if isEnabled, removalTimerCancellable == nil {
91-
removalTimerCancellable = Foundation
92-
.Timer
93-
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate, on: .main, in: .default)
94-
.autoconnect()
89+
removalTimerCancellable = DefaultTimer
90+
.publish(every: ScreenPropertiesAdapter.currentValue.refreshRate)
9591
.sink { [weak self] _ in self?.removeItemsIfRequired() }
9692
} else if !isEnabled, removalTimerCancellable != nil {
9793
removalTimerCancellable?.cancel()

Sources/StreamVideo/Utils/ScreenPropertiesAdapter/ScreenPropertiesAdapter.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import UIKit
1111

1212
public final class ScreenPropertiesAdapter: @unchecked Sendable {
1313

14-
public private(set) var preferredFramesPerSecond: Int = 0
15-
public private(set) var refreshRate: TimeInterval = 0
16-
public private(set) var scale: CGFloat = 0
14+
public private(set) var preferredFramesPerSecond: Int = 30
15+
public private(set) var refreshRate: TimeInterval = 0.16
16+
public private(set) var scale: CGFloat = 1
1717

1818
init() {
1919
Task { @MainActor in
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Combine
6+
import Foundation
7+
8+
/**
9+
Default real-world implementations of the ``Timer`` protocol.
10+
11+
These timers are based on ``DispatchQueue`` and ``DispatchSourceTimer``,
12+
allowing precise control of scheduling on custom queues. This ensures that
13+
timers are not bound to the main thread and can run in background contexts.
14+
*/
15+
public struct DefaultTimer: Timer {
16+
/// Schedules a one-shot timer that fires once after the specified interval.
17+
///
18+
/// The timer executes the ``onFire`` callback on the specified dispatch queue,
19+
/// which can be any background or custom queue. Timers are not tied to the
20+
/// main thread and can run on any queue you provide.
21+
///
22+
/// - Parameters:
23+
/// - timeInterval: Delay in seconds before the timer fires.
24+
/// - queue: The dispatch queue on which to execute ``onFire``. This can be
25+
/// any queue, including background queues.
26+
/// - onFire: Callback to invoke when the timer fires.
27+
/// - Returns: A ``TimerControl`` used to cancel the timer if needed.
28+
@discardableResult
29+
static func schedule(
30+
timeInterval: TimeInterval,
31+
queue: DispatchQueue,
32+
onFire: @escaping () -> Void
33+
) -> TimerControl {
34+
let worker = DispatchWorkItem(block: onFire)
35+
queue.asyncAfter(deadline: .now() + timeInterval, execute: worker)
36+
return worker
37+
}
38+
39+
/// Schedules a repeating timer on the given queue.
40+
///
41+
/// The timer fires repeatedly at the specified interval. Execution happens
42+
/// on the provided dispatch queue and is not tied to the main thread. You can
43+
/// use any custom or background queue for timer events.
44+
///
45+
/// - Parameters:
46+
/// - timeInterval: Interval in seconds between timer firings.
47+
/// - queue: The dispatch queue used to invoke the ``onFire`` callback.
48+
/// - onFire: Callback to invoke on each timer fire.
49+
/// - Returns: A ``RepeatingTimerControl`` used to manage the timer.
50+
static func scheduleRepeating(
51+
timeInterval: TimeInterval,
52+
queue: DispatchQueue,
53+
onFire: @escaping () -> Void
54+
) -> RepeatingTimerControl {
55+
RepeatingTimer(timeInterval: timeInterval, queue: queue, onFire: onFire)
56+
}
57+
58+
/// Returns a Combine publisher that emits `Date` values at a fixed interval.
59+
///
60+
/// The timer operates on a background queue and only emits values while
61+
/// there are active subscribers. If the interval is less than or equal to
62+
/// zero, a warning is logged and a single `Date` value is emitted instead.
63+
///
64+
/// - Parameters:
65+
/// - interval: Time between emitted date values.
66+
/// - file: The file from which the method is called. Used for logging.
67+
/// - function: The function from which the method is called.
68+
/// - line: The line number from which the method is called.
69+
/// - Returns: A publisher that emits dates while subscribed.
70+
public static func publish(
71+
every interval: TimeInterval,
72+
file: StaticString = #file,
73+
function: StaticString = #function,
74+
line: UInt = #line
75+
) -> AnyPublisher<Date, Never> {
76+
guard interval > 0 else {
77+
log.warning(
78+
"Interval cannot be 0 or less",
79+
functionName: function,
80+
fileName: file,
81+
lineNumber: line
82+
)
83+
return Just(Date()).eraseToAnyPublisher()
84+
}
85+
return TimerPublisher(interval: interval).eraseToAnyPublisher()
86+
}
87+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
extension DispatchWorkItem: TimerControl {}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Combine
6+
import Foundation
7+
8+
/// A protocol describing a timing utility for scheduling one-shot and repeating
9+
/// timers, as well as retrieving the current time or publishing timer events.
10+
///
11+
/// All scheduled timers are executed on the provided dispatch queue, which
12+
/// allows running timers off the main thread in background contexts.
13+
protocol Timer {
14+
15+
/// Schedules a new one-shot timer on the specified queue.
16+
///
17+
/// The timer fires once after the given time interval. The `onFire` callback
18+
/// is invoked on the provided dispatch queue, which is not restricted to the
19+
/// main thread.
20+
///
21+
/// - Parameters:
22+
/// - timeInterval: Seconds until the timer fires.
23+
/// - queue: The queue where the `onFire` callback is executed. It can be a
24+
/// background queue.
25+
/// - onFire: Callback triggered when the timer fires.
26+
/// - Returns: A `TimerControl` that can cancel the scheduled timer.
27+
@discardableResult
28+
static func schedule(
29+
timeInterval: TimeInterval,
30+
queue: DispatchQueue,
31+
onFire: @escaping () -> Void
32+
) -> TimerControl
33+
34+
/// Schedules a new repeating timer on the specified queue.
35+
///
36+
/// The timer repeatedly fires after the specified time interval. The `onFire`
37+
/// callback is invoked on the provided dispatch queue, which can be a
38+
/// background queue and is not tied to the main thread.
39+
///
40+
/// - Parameters:
41+
/// - timeInterval: Seconds between repeated timer firings.
42+
/// - queue: The queue on which the `onFire` callback is executed.
43+
/// - onFire: Callback triggered each time the timer fires.
44+
/// - Returns: A `RepeatingTimerControl` that allows suspension and resumption.
45+
static func scheduleRepeating(
46+
timeInterval: TimeInterval,
47+
queue: DispatchQueue,
48+
onFire: @escaping () -> Void
49+
) -> RepeatingTimerControl
50+
51+
/// Returns the current system date and time.
52+
static func currentTime() -> Date
53+
54+
/// Returns a publisher that emits timer events at the specified interval.
55+
///
56+
/// This publisher is designed to emit values from a background context and
57+
/// is not restricted to the main thread.
58+
///
59+
/// - Parameters:
60+
/// - interval: Time between emitted `Date` values.
61+
/// - repeating: A Boolean indicating if the timer should repeat.
62+
/// - Returns: A publisher that emits the current `Date` on each fire.
63+
static func publish(
64+
every interval: TimeInterval,
65+
file: StaticString,
66+
function: StaticString,
67+
line: UInt
68+
) -> AnyPublisher<Date, Never>
69+
}
70+
71+
extension Timer {
72+
73+
/// Returns the current system date and time.
74+
static func currentTime() -> Date {
75+
Date()
76+
}
77+
}

0 commit comments

Comments
 (0)