Skip to content

[Performance]Break async stream memory leaks #826

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

Conversation

ipavlidakis
Copy link
Contributor

@ipavlidakis ipavlidakis commented May 27, 2025

🎯 Goal

AsyncStreams are causing leaks on continuations. This revision attempts to fix that by backing AsyncStream on StreamVideo, by Combine Publishers

🧪 Manual Testing Notes

Given When Then
🟠 In a call alone I enable the camera The camera has a flipping animation
🟠 In a call I rotate the device The app freezes and then kills the phone
🟠 In a call I set a background filter My video track gets stuck and I get disconnected, but UI is frozen
🟢 In a call with 4+ participants I raise a hand It does not show for me, but it does for web participants
🟠 In a call with >2 participants I move app to background and then foreground All video tracks appear frozen
🟠 In a call with >2 participants I move app to background and then foreground Camera and mic buttons do not work

✅ Call Test Summary

Scenario

1:1 call for 5 minutes

  • noise-cancellation: active
  • background-filter: inactive
  • profiler: connected
  • debugger: disconnected

🧵 Tasks

Created Average Active Average Alive
29309 3 13,000

🌡️ Thermal State Degradation

Start End Thermal State
00:00.000 00:55.710 Nominal thermal state
00:55.710 01:25.708 Fair thermal state
01:25.708 05:16.416 Serious thermal state

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

@ipavlidakis ipavlidakis self-assigned this May 27, 2025
@ipavlidakis ipavlidakis added the enhancement New feature or request label May 27, 2025
@ipavlidakis ipavlidakis marked this pull request as ready for review May 27, 2025 18:21
@ipavlidakis ipavlidakis requested a review from a team as a code owner May 27, 2025 18:21
@ipavlidakis ipavlidakis changed the base branch from develop to fix/performance-improvements May 27, 2025 18:21
@ipavlidakis ipavlidakis changed the title Fix/performance improvements subtasks/break async stream memory leaks [Performance]Break async stream memory leaks May 27, 2025
@ipavlidakis ipavlidakis mentioned this pull request May 27, 2025
12 tasks
Copy link

Public Interface

+ extension ScreenPropertiesAdapter: InjectionKey  
+ 
+   public nonisolated static var currentValue: ScreenPropertiesAdapter

+ public final class ScreenPropertiesAdapter: @unchecked Sendable  
+ 
+   public private var preferredFramesPerSecond: Int
+   public private var refreshRate: TimeInterval
+   public private var scale: CGFloat

+ public protocol TimerProviding



 public class StreamVideo: ObservableObject, @unchecked Sendable  
-   public var state: State
+   case ringEventReceived
-   public let videoConfig: VideoConfig
+   
-   public var user: User
+ 
-   public var isHardwareAccelerationAvailable: Bool
+   public var state: State
-   public lazy var rejectionReasonProvider: RejectionReasonProviding
+   public let videoConfig: VideoConfig
-   
+   public var user: User
- 
+   public var isHardwareAccelerationAvailable: Bool
-   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
+   public lazy var rejectionReasonProvider: RejectionReasonProviding
-   public func connect()async throws 
+   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
-   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
+   
-   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
+ 
-   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
+   public func connect()async throws 
-   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
+   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
-   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
+   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
-   public func listDevices()async throws -> [Device]
+   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
-   public func disconnect()async 
+   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
-   public func subscribe()-> AsyncStream<VideoEvent>
+   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
-   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
+   public func listDevices()async throws -> [Device]
-   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func disconnect()async 
-   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func eventPublisher()-> AnyPublisher<VideoEvent, Never>
-   
+   public func eventPublisher(for event: WSEvent.Type)-> AnyPublisher<WSEvent, Never>
- 
+   public func subscribe()-> AsyncStream<VideoEvent>
-   public final class State: ObservableObject, @unchecked Sendable  
+   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
-   
+   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var connection: ConnectionStatus
+   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var user: User
+   
-     @Published public internal var activeCall: Call?
+ 
-     @Published public internal var ringingCall: Call?
+   public final class State: ObservableObject, @unchecked Sendable  
+   
+     @Published public internal var connection: ConnectionStatus
+     @Published public internal var user: User
+     @Published public internal var activeCall: Call?
+     @Published public internal var ringingCall: Call?

@Stream-SDK-Bot
Copy link
Collaborator

Stream-SDK-Bot commented May 27, 2025

SDK Size

title develop branch diff status
StreamVideo 7.54 MB 7.54 MB +3 KB 🟢
StreamVideoSwiftUI 2.26 MB 2.27 MB +17 KB 🟢
StreamVideoUIKit 2.38 MB 2.39 MB +16 KB 🟢
StreamWebRTC 9.85 MB 9.85 MB 0 KB 🟢

Copy link

Public Interface

+ extension ScreenPropertiesAdapter: InjectionKey  
+ 
+   public nonisolated static var currentValue: ScreenPropertiesAdapter

+ public final class ScreenPropertiesAdapter: @unchecked Sendable  
+ 
+   public private var preferredFramesPerSecond: Int
+   public private var refreshRate: TimeInterval
+   public private var scale: CGFloat

+ public protocol TimerProviding



 public class StreamVideo: ObservableObject, @unchecked Sendable  
-   public var state: State
+   case ringEventReceived
-   public let videoConfig: VideoConfig
+   
-   public var user: User
+ 
-   public var isHardwareAccelerationAvailable: Bool
+   public var state: State
-   public lazy var rejectionReasonProvider: RejectionReasonProviding
+   public let videoConfig: VideoConfig
-   
+   public var user: User
- 
+   public var isHardwareAccelerationAvailable: Bool
-   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
+   public lazy var rejectionReasonProvider: RejectionReasonProviding
-   public func connect()async throws 
+   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
-   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
+   
-   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
+ 
-   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
+   public func connect()async throws 
-   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
+   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
-   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
+   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
-   public func listDevices()async throws -> [Device]
+   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
-   public func disconnect()async 
+   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
-   public func subscribe()-> AsyncStream<VideoEvent>
+   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
-   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
+   public func listDevices()async throws -> [Device]
-   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func disconnect()async 
-   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func eventPublisher()-> AnyPublisher<VideoEvent, Never>
-   
+   public func eventPublisher(for event: WSEvent.Type)-> AnyPublisher<WSEvent, Never>
- 
+   public func subscribe()-> AsyncStream<VideoEvent>
-   public final class State: ObservableObject, @unchecked Sendable  
+   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
-   
+   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var connection: ConnectionStatus
+   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var user: User
+   
-     @Published public internal var activeCall: Call?
+ 
-     @Published public internal var ringingCall: Call?
+   public final class State: ObservableObject, @unchecked Sendable  
+   
+     @Published public internal var connection: ConnectionStatus
+     @Published public internal var user: User
+     @Published public internal var activeCall: Call?
+     @Published public internal var ringingCall: Call?

Copy link

Public Interface

+ extension ScreenPropertiesAdapter: InjectionKey  
+ 
+   public nonisolated static var currentValue: ScreenPropertiesAdapter

+ public final class ScreenPropertiesAdapter: @unchecked Sendable  
+ 
+   public private var preferredFramesPerSecond: Int
+   public private var refreshRate: TimeInterval
+   public private var scale: CGFloat

+ public protocol TimerProviding



 public class StreamVideo: ObservableObject, @unchecked Sendable  
-   public var state: State
+   case ringEventReceived
-   public let videoConfig: VideoConfig
+   
-   public var user: User
+ 
-   public var isHardwareAccelerationAvailable: Bool
+   public var state: State
-   public lazy var rejectionReasonProvider: RejectionReasonProviding
+   public let videoConfig: VideoConfig
-   
+   public var user: User
- 
+   public var isHardwareAccelerationAvailable: Bool
-   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
+   public lazy var rejectionReasonProvider: RejectionReasonProviding
-   public func connect()async throws 
+   public convenience init(apiKey: String,user: User,token: UserToken,videoConfig: VideoConfig = VideoConfig(),pushNotificationsConfig: PushNotificationsConfig = .default,tokenProvider: UserTokenProvider? = nil)
-   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
+   
-   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
+ 
-   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
+   public func connect()async throws 
-   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
+   public func call(callType: String,callId: String,callSettings: CallSettings? = nil)-> Call
-   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
+   public func makeCallsController(callsQuery: CallsQuery)-> CallsController
-   public func listDevices()async throws -> [Device]
+   @discardableResult public func setDevice(id: String)async throws -> ModelResponse
-   public func disconnect()async 
+   @discardableResult public func setVoipDevice(id: String)async throws -> ModelResponse
-   public func subscribe()-> AsyncStream<VideoEvent>
+   @discardableResult public func deleteDevice(id: String)async throws -> ModelResponse
-   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
+   public func listDevices()async throws -> [Device]
-   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func disconnect()async 
-   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
+   public func eventPublisher()-> AnyPublisher<VideoEvent, Never>
-   
+   public func eventPublisher(for event: WSEvent.Type)-> AnyPublisher<WSEvent, Never>
- 
+   public func subscribe()-> AsyncStream<VideoEvent>
-   public final class State: ObservableObject, @unchecked Sendable  
+   public func subscribe(for event: WSEvent.Type)-> AsyncStream<WSEvent>
-   
+   public func queryCalls(next: String? = nil,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var connection: ConnectionStatus
+   public func queryCalls(filters: [String: RawJSON]?,sort: [SortParamRequest] = [SortParamRequest.descending("created_at")],limit: Int? = 25,watch: Bool = false)async throws -> (calls: [Call], next: String?)
-     @Published public internal var user: User
+   
-     @Published public internal var activeCall: Call?
+ 
-     @Published public internal var ringingCall: Call?
+   public final class State: ObservableObject, @unchecked Sendable  
+   
+     @Published public internal var connection: ConnectionStatus
+     @Published public internal var user: User
+     @Published public internal var activeCall: Call?
+     @Published public internal var ringingCall: Call?

Copy link

@ipavlidakis ipavlidakis merged commit 56ad08e into fix/performance-improvements May 28, 2025
12 checks passed
@ipavlidakis ipavlidakis deleted the fix/performance-improvements-subtasks/break-async-stream-memory-leaks branch May 28, 2025 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants