danielrhodes / Swift-ActionCableClient

ActionCable is a new WebSocket server being released with Rails 5 which makes it easy to add real-time features to your app. This Swift client makes it dead-simple to connect with that server, abstracting away everything except what you need to get going.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Creating channel blocks main thread

danipralea opened this issue · comments

Hello again @danielrhodes
I found out that creating a Channel sometime blocks the main thread.
My code:

webSocketsRoomChannel = SocketRouter.shared.client.create(WebSocketEventsChannel.channel, identifier: channelIdPayload, autoSubscribe: true, bufferActions: true)

The stack:
screenshot 2016-12-01 18 39 38

I will come up with a fix if I find one
Update:
screenshot 2016-12-01 19 08 48

Is this an infinite cycle?

Have you retained the channel anywhere?

it's a simple variable on the conversations view controller

var webSocketsRoomChannel: Channel?

What version are you using? Something similar to the above used to happen, especially if the the channel had not been retained anywhere. But that should be fixed now.

- ActionCableClient (0.2.2):
    - Starscream (~> 2.0.1)

The only problem is that the issue is not consistent. It's very rare for me, but of course - very often for the testers

Do you have some sample code for how you are setting up everything and in the view controller. Would be happy to take that and test it on my end.

class ConversationViewController: UIViewController {
    var webSocketsRoomChannel: Channel?
    func subscribeToWebsocketChannel() {
        guard SocketRouter.shared.isConnected, let channel = viewModel.channel, let identifier = channel.identifier else {
            print("socket is not connected or data is not sufficient")
            return
        }
        let channelIdPayload = ["channel_id" : identifier]
       webSocketsRoomChannel = SocketRouter.shared.client.create(WebSocketEventsChannel.channel, identifier: channelIdPayload, autoSubscribe: true, bufferActions: true)
        
        // Receive a message from the server. Typically a Dictionary.
        webSocketsRoomChannel?.onReceive = { (JSONResponse : Any?, error : Error?) in
            print("Received", JSONResponse ?? "N/A", error ?? "no error")
            
        }
        
        // A channel has successfully been subscribed to.
        webSocketsRoomChannel?.onSubscribed = {
            print("Yay! You're subscribed!")
        }
        
        // A channel was unsubscribed, either manually or from a client disconnect.
        webSocketsRoomChannel?.onUnsubscribed = {
            print("Unsubscribed")
        }
        
        // The attempt at subscribing to a channel was rejected by the server.
        webSocketsRoomChannel?.onRejected = {
            print("Rejected")
        }
    }
    
    func removeWebSocketsNotifications() {
        // Remove channel subscription
        webSocketsRoomChannel?.unsubscribe()
        // Stop listening notification
        NotificationCenter.default.removeObserver(self)
    }
    
    func addWebSocketsNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(webSocketConnected(_:)), name: NSNotification.Name(rawValue: WebsocketDidConnectNotification), object: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(webSocketDisconnected(_:)), name: NSNotification.Name(rawValue: WebsocketDidDisconnectNotification), object: nil)
    }
    
    func webSocketConnected(_ notification: NSNotification) {
        print("\(self.className): web socket connected")
        if let roomChannel = webSocketsRoomChannel, roomChannel.isSubscribed == false {
            subscribeToWebsocketChannel()
        }
    }
    
    func webSocketDisconnected(_ notification: NSNotification) {
        print("\(self.className): web socket disconnected")
    }
}

So from the looks of Instruments above, it seems like either you were running the application for a very long time or you might be calling subscribeToWebsocketChannel very very often. The code you highlighted is simply doing a Dictionary lookup, which isn't blocking, and is O(1), so I'm not sure how that would be causing a problem.

I did, however, find that there was some JSON encoding being done from the calling thread (which I assume is main in this case), and so for whatever reason, this might be the source of the time being taken above (sometimes Instruments only makes an educated guess about where in the code things happen).

Try out the json_encoding_on_calling_thread_fix branch and tell me how that worked for you.

https://github.com/danielrhodes/Swift-ActionCableClient/tree/json_encoding_on_calling_thread_fix

In your Podfile, you can do the following:

pod 'ActionCableClient', :git => 'https://github.com/danielrhodes/Swift-ActionCableClient.git', :branch => 'json_encoding_on_calling_thread_fix'

Note that I had to change the API slightly, but now there is a callback on the channel.action(:) method telling you if the action was successfully sent or an error occurred. There is an example on the main README and in the header docs.

I have it again:
screenshot 2016-12-06 19 06 08

I will try with the json_encoding_on_calling_thread_fix branch as well and let you know how it goes.
Thanks a lot for the support.

PS: subscribeToWebsocketChannel is done only once
PPS: The problem is not with sending an action, it's with creating the channel

@danielrhodes the new method of sending an action doesn't call the callback when an error occurs

The old way of doing it was working fine (throwing an error)

Do you mean on the server side or client side? If the error is on the server side, there is no way for the client to know an error occurred.

on client side. all the problems I have are client-side.

What was the specific error you were looking for?

creating a channel blocks the main thread

How many times are you calling this over the course of runtime? And how many total channels do you have? I ran the example app and put client.create in a 5000 iteration loop (on the main thread) and profiled it to see if anything weird came up.

screen shot 2016-12-08 at 17 19 42

Nothing there indicates anything amiss given how many times it was called. In fact, most of that would only occur once because it checks to see if there's already a channel. So that reduces the performance bottleneck to doing a lookup in a dictionary. This seems consistent with your Instruments trace, but I can't see how that would come up if something strange weren't also occurring.

Perhaps a Dictionary lookup could cause a performance problem if there were an unusually large number of objects or were called in a very tight loop. Swift does not necessarily have the most optimized version of this, but it's certainly not a blocking operation.

I'm only doing it once

So you only go into that ViewController once the entire time the app is running? And it completely blocks the main thread?

From looking at your code above, can you try calling subscribeToWebsocketChannel() in viewDidLoad and removing those NSNotification observers in addWebSocketsNotifications. You can subscribe to a channel before the client connects and it will automatically subscribe once a connection occurs, so most that logic is unnecessary.

I will definitely try that.
I have a list of channels, which I can select. And sometimes, very randomly - it completely freezes like that.

Thank you for your support. I will try and let you know how it goes

@danielrhodes there's a retain cycle in the create function:
screenshot 2017-01-19 16 35 43
screenshot 2017-01-19 16 36 53