-
Notifications
You must be signed in to change notification settings - Fork 221
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
Rework to remove implicit flushes #357
Conversation
d1021cd
to
37a2e20
Compare
37a2e20
to
0203a18
Compare
Thank you very much for the improvement! Technically this looks fine. But there are two small issues. First, does automatic Pong reply still works correctly? It used to rely on automatic flush while reading. Also, does it work correctly if there is already some message in outgoing buffer? I'm quite sure I introduced the outgoing queue mostly for handling pings correctly. Second, I'd rather rename |
Auto pong replies should still auto flush. Previously In this PR So we flush eagerly whenever we have a auto generated pong or close to send otherwise we only flush on user call. Related: If the user sends a manual pong there is no implicit flushing, since if the user writes they can also flush as normal. My thinking was implicit/auto flushing only really made sense for auto generated messages outside of the user's direct control.
That's a good idea. Perhaps we have methods We could also keep a deprecated |
Yes, How does Pong work now if there is something not yet flushed and a Ping is incoming? This is the only thing that has to be verified. Probably it is covered by Autobahn test suite but I'd like to double check this before merging. |
Ok I'll make these modifications when I get a moment.
Once a ping is read a pong is scheduled to be written and flushed on the next call to read or write. On that call, pong is appended to the |
3107e38
to
b2069f8
Compare
I have renamed the methods to better reflect the changes. We now use I've added entries to the changelog To aid migration I have deprecated |
b2069f8
to
ae66c81
Compare
Refine docs Add `send` method Add deprecated versions of write_message, write_pending, read_message Handle pong WriteBufferFull error Add changelog
ae66c81
to
84a54b7
Compare
This looks good, passes all tests, so merged. Let's see how it behaves in real life. |
Great to see merged, thanks for reviewing! I've prepared snapview/tokio-tungstenite#284 for the merged |
My context: I've been working on a websocket load generator service that will write lots of small json message events to it's ws clients. I was interested in having a high peak throughput from this service to allow me to test downstream load performance. It uses axum->tokio-tungstenite->tungstenite. Doing localhost testing I got ~1.2M msg/s peak throughput. This PR improves that to ~2M msg/s.
Because of this improvement I thought this PR may be worthwhile.
Summary: Reduce flushing and allow better control of when flushes happen
I noticed there was a lot of flushing happening. Currently flushing is done something like:
This seems excessive particularly when trying to send batches of lots of small messages. This PR minimises flushes so that a flush will only occur if
write_pending
is called, or if a "system message" (pong/close) should be dispatched. So users can callwrite_message
many times without flushing (which is more like how I expected it to behave), then callwrite_pending
which will flush.Impl notes
My first approach was to remove the flush before each frame write currently in
WebSocketContext::write_pending
And remove the flush after each
FrameCodec::write_frame
. Then wire upwrite_message
to not call flush andwrite_pending
to essentially be flush.Flush before write &
send_queue
I noticed that after these changes the
WebSocketContext::send_queue
no longer really seemed to do much. The logic as is actually requires flushing before each write to work otherwise a message is simply added to thesend_queue
then taken off it and put in theFrameCodec::out_buffer
.So I removed the
send_queue
since it no longer does anything. It didn't seem to do too much previously, as it could only build up there if flushing before each write returned an error. If the write itself returned an error the send_queue would be empty and the message will simply be stored in theFrameCodec::out_buffer
. I'm not sure what the benefit of having both a send_queue and an out_buffer is/was?This PR I replace the
send_queue
by using the existingout_buffer
. Writes are buffered there without flushing before each. We can buffer many writes here, as was done before, but without any send queue.I replaced the
max_send_queue
config withmax_write_buffer_size
which can bound the previously unboundout_buffer
. If a write would exceed this bound the message is instead returned as an error, in a very similar way previous logic would handle thesend_queue
bound. The one difference here is the messages is returned as aMessage::Frame
since it has already been serialized at this point. This seems worse than before where the message was returned as it was given. However, logic to re-send should still work. I'm not sure this difference is worth the flush call to prevent. It might be possible to put in a conversion back to the old message somehow but I'm not sure it's worth it.Flush after each write
Removing this seems straightforward. We now require the caller calls
write_pending
after any writes to guarantee it will be flushed. This requirement seems similar to most write interfaces.An exception is "system messages" like pong responses. If these have been scheduled we'll still automatically flush.
Flush on each read
Currently
WebSocketContext::read_message
will flush writes on each call.This means that if my service is polling for incoming messages I can still end up flushing writes a lot more than I'd expect. This PR changes this to only write if we actually have a pong/close "system message" to send. Since system messages won't scale with load this minimises extra flush impact while still writing-and-flushing these messages asap.
micro-bench
I added a micro-bench
benches/write.rs
to reproduce flush cost. This simulates aWrite
impl that takes 100ns to flush. The scenario is 100kwrite_message
s then onewrite_pending
. E.g. how I would approach writing a batch of small messages with the current API.The improvement is clear since this PR will call only one flush per batch.
Issues
Error::WriteBufferFull
will return the message as aMessage::Frame
, whereasSendQueueFull
would return the message as given.write_pending
toflush
?