paullouisageneau / datachannel-wasm

C++ WebRTC Data Channels and WebSockets for WebAssembly in browsers
MIT License
148 stars 25 forks source link

onBufferedAmountLow is annoying #25

Closed jeffRTC closed 2 years ago

jeffRTC commented 3 years ago

I made a wrapper around dc.send that automatically schedule the message to be send once onBufferedAmountLow event received.

But the problem is that this event not always get invoked as I expected thus leaving messages not being able to send.

class DataChannelWrapper {
    private:
        int requestCount{};

        std::queue<std::string> messageQueue;
    public:
        std::shared_ptr<rtc::DataChannel> dc;

        void onBufferedAmountLow();

        bool send(std::string message);
};

inline bool DataChannelWrapper::send(std::string message) {
    if (dc)
    {
        if(requestCount == 0) {       
            bool status = dc->send(reinterpret_cast<rtc::byte *>(message.data()), message.size());

            if(status) {

                requestCount++;
            }

            return status;
        } else {
            requestCount++;

            // Schedule 
            messageQueue.push(message);

            return true;
        }
    } else {
        std::cout << "Shared Pointer is null" << "\n";

        return false;
    }
}

inline void DataChannelWrapper::onBufferedAmountLow() {
    std::cout << "onBufferedAmountLow reached" << "\n";

    if(messageQueue.empty()) {
        std::cout << "Empty Message Queue" << "\n";

        return;
    }

    auto message = messageQueue.front();

    dc->send(reinterpret_cast<rtc::byte *>(message.data()), message.size());

    std::cout << "Sent a message from Queue" << "\n";

    messageQueue.pop();

    requestCount--;
}

What can be done to fix this?

paullouisageneau commented 3 years ago

The WebRTC bufferedamountlow event does not actually work that way, as there is no guarantee it will be called after send(). It should actually be called when bufferedAmount goes back under bufferedAmountLowThreshold (default 0), as the WebRTC specification says:

bufferedamountlow: The RTCDataChannel object's bufferedAmount decreases from above its bufferedAmountLowThreshold to less than or equal to its bufferedAmountLowThreshold.

So in the end, it boills down to how the browser calculates bufferedAmount. In practice, a browser is free to implement sending as it pleases, so it may not increment bufferedAmount if the message can be sent immediately, therefore send() won't necessarily result in a bufferedamountlow event (which is subject to change, see this bug for instance).

Therefore, the correct usage of the onBufferedAmountLow callback is as follows (provided bufferedAmountLowThreshold is 0):

jeffRTC commented 2 years ago

@paullouisageneau

I understood your idea, but

When a new message should be sent, put it in the queue and send queued messages until bufferedAmount() > 0 When the onBufferedAmountLow callback is called, send queued messages until bufferedAmount() > 0

This sounds asyncish to me. When I want to send a new message, I should put it in queue, but when should I front and pop from queue?

Stackoverflow posts also had mentioned the difficulty of onBufferedAmountLow and they used setTimeout with specific ms to send messages and it looks like your idea goes same way.

paullouisageneau commented 2 years ago

This sounds asyncish to me. When I want to send a new message, I should put it in queue, but when should I front and pop from queue?

What do you mean by asyncish? Of course the send method has to be non-blocking in the end, since you may not block the browser event loop.

You should pop from the queue both after you put a new message in the queue, and when onBufferedAmountLow is called. The pseudo code is rather simple:

queue<string> q;

void flush() {
    while (!q.empty() && dc->bufferedAmount() == 0) {
        dc->send(move(q.front()));
        q.pop();
    }
}

void send(string message) { // call this when you want to send a message
    q.push(move(message));
    flush();
}

dc->bufferedAmountLowThreshold(0);
dc->onBufferedAmountLow(flush);

Stackoverflow posts also had mentioned the difficulty of onBufferedAmountLow and they used setTimeout with specific ms to send messages and it looks like your idea goes same way.

The JavaScript API is awkwardly designed, but it's not that difficult. Indeed, setTimeout can be a useful refinement in the specific case you want to send a big and synchronous source of data as fast as possible.

In that case, there is no "a new message should be sent" situation, you always want to send and everything is virtually in the queue from the start. Therefore, the "send queued messages until bufferedAmount() > 0" loop might freeze the browser event loop for a while if the queue is big and bufferedAmount never increases because the throughput is very high.

A solution to this issue is to limit the maximum amount of data the loop can send in one call, and if it reaches the maximum, schedule it to be called again after a short delay, giving a chance to the event loop to run.

jeffRTC commented 2 years ago

According to your pseudo code, I don't see onBufferedAmountLow get chance to send any messages since the flush invocation of send method grab that chance.

jeffRTC commented 2 years ago

the "send queued messages until bufferedAmount() > 0" loop might freeze the browser event loop for a while if the queue is big and bufferedAmount never increases because the throughput is very high.

By the way, I'm sending 64k chunks so is this kind of throttling needed here?

paullouisageneau commented 2 years ago

According to your pseudo code, I don't see onBufferedAmountLow get chance to send any messages since the flush invocation of send method grab that chance.

If dc->bufferedAmount() > 0, flush() will do nothing, an actual send will happen only when onBufferedAmountLow is called (it will be called when dc->bufferedAmount() == 0).

By the way, I'm sending 64k chunks so is this kind of throttling needed here?

It is irrelevant of the size of messages. It's needed only if your use case is sending a huge file as fast as possible.

jeffRTC commented 2 years ago

@paullouisageneau I used your code as-it is and with better variable names. I see that onBufferedAmountLow never get called and that only 1 message get out.

while(!messageQueue.empty() && dc && dc->bufferedAmount() > 0)

I suspect dc->bufferedAmount() > 0 might be causing this or dc->setBufferedAmountLowThreshold(0)

jeffRTC commented 2 years ago

onBufferedAmountLow get called if I send messages without your code. Just sending messages without queueing aka straightly.

paullouisageneau commented 2 years ago

while(!messageQueue.empty() && dc && dc->bufferedAmount() > 0)

It should be dc->bufferedAmount() == 0, like in my example code, instead of dc->bufferedAmount() > 0.

jeffRTC commented 2 years ago

@paullouisageneau

It works now and feel somewhat more performant. Any idea why It become faster?

Closing.

paullouisageneau commented 2 years ago

Any idea why It become faster?

Probably because using the callback properly like this ensures you send as soon as possible, so there is no idle time.