async_to_sync(channel_layer.group_send) CPU and Memory Usage

Hi everyone. I use django-channels for playing PCM audio data retrieved from UDP-server. It works, but sound is played with 1 second delay and what is more suspicious is that sending sound data to a web browser takes 10% of the CPU. It’s growing memory usage constantly (though after the audio finishes playing memory is released).
The traffic is approximately 40 packets per second, and the length of a packet is 400 bytes. It was tested using InMemoryChannelLayer and RedisChannelLayer.

Other actions occurring between data received from UDP and sent to websockets (channel_layer.group_send) don’t affect CPU and memory. To test this I just commented out async_to_sync(channel_layer.group_send). It showed at most 1% of CPU usage.

Here is the code of sending:

        channel_layer = get_channel_layer()
        private_room_name = ConsumerUtils().get_private_channel_name(chat_handler.ui_client.username)
        async_to_sync(channel_layer.group_send)(
            private_room_name,
            {
                'type': 'chat_message',
                'message': sound_bytes
            }
        )

and the consumer:

class TheConsumer(WebsocketConsumer):
..............................
    def chat_message(self, event):
        """
        It sends voice data to a client
        :param event: a voice message as bytes
        """
        self.send(bytes_data=event['message'])

I’d appreciate any hints or suggestions.
Thanks in advance.
Andrii.

<conjecture>
My gut reaction to this is that you absolutely want to make sure that your listener that is receiving this data and your consumer are all pure async. The overhead of starting up event loops when necessary is likely to be quite significant, and the more consumers you have rebroadcasting this data, the worse it’s going to be.
</conjecture>

1 Like

Thanks for the quick reply. You mentioned: “The overhead of starting up event loops when necessary is likely to be quite significant”. Could you explain at which moment the event loop is starting: when a new WebSocket client is connected or when I call group_send?

It’s the async_to_sync method that will create a new event loop if it needs to.

Yes, it is in the statement at:

But it’s in the async_to_sync call and not the group_send function.

Do you think there’s a good solution? Would Async-consumer help?

That’s what I’m recommending:

It’s my opinion (and that’s all it is - I don’t have a way at the moment to reasonably test this) that by making your consumer async you’re going to reduce the “friction” that exists by switching between threaded and event-loop code.

Now, if you’ve got some sync-specific code that needs to be called by that consumer, that’s a different issue. You could be creating that same friction, but in reverse. In that situation, I’d be looking at a different solution.

I’d also be looking at a different way of structuring this:

If you’re calling this 40 times / second, there’s a lot of repeated calls being made to get the channel layer and private room name. (But without seeing the complete block of code, it’s difficult for me to offer an alternative.)

At an absolute minimum, I would make all consumers join a common group. Then, your sending code sends that data to that group, allowing Channels to route the data through to the individual consumers.

Thanks for the valuable advice!
Since I need to send data only to one client I tried to store the consumer when connected in the user-related object and send data directly as follows:

chat_handler.ui_client.consumer.send(bytes_data=sound_bytes)

The CPU usage decreased to 3-5%, memory usage did not increase.
Acceptable result!

Again, thanks a million.