Skip to content
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

change incoming_message to bytearray to improve += performance #15

Conversation

Mitch-Martinez
Copy link

Using += on bytes is inefficient because bytes is immutable. Making incoming_message a bytearray allows += to efficiently reallocate memory for incoming_message when incoming messages are large.

It is not the usual use case for websockets but when a client attempts to send a 50MB file the += operation on a bytes object begins to become extremely expensive. After modifying the imcoming_message object it greatly improved the performance of reading large messages, mainly ones greater than 10MB.

@miguelgrinberg
Copy link
Owner

The code that you edited handles both text and binary messages, so it cannot be changed to bytearray as that breaks text.

@Mitch-Martinez
Copy link
Author

Mitch-Martinez commented Jun 24, 2022 via email

@Mitch-Martinez
Copy link
Author

Made an update to differentiate between text and binary data.

I opted to split checking for ByteMessage and TextMessage to avoid always having to call isinstance() twice.

Would returning bytearray vs bytes be an issue for anything using this module? I would assume anybody calling receive would appreciate that the returned data is now mutable, but I could definitely be missing something.

@Mitch-Martinez
Copy link
Author

Update to fix flake8 errors, I separated test_recieve_large in the server test so it should now get coverage of the entire diff. I appreciate your patience with all this.

@codecov-commenter
Copy link

Codecov Report

Merging #15 (21045e3) into main (9e024fd) will increase coverage by 0.17%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main      #15      +/-   ##
==========================================
+ Coverage   97.66%   97.83%   +0.17%     
==========================================
  Files           2        2              
  Lines         171      185      +14     
  Branches       35       39       +4     
==========================================
+ Hits          167      181      +14     
  Misses          3        3              
  Partials        1        1              
Impacted Files Coverage Δ
src/simple_websocket/ws.py 97.82% <100.00%> (+0.17%) ⬆️

📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more

@miguelgrinberg
Copy link
Owner

Would returning bytearray vs bytes be an issue for anything using this module?

I'm not sure. I need to find out if bytearray(some_bytes) and bytes(some_bytearray) are cheap operations, because in that case the array can be converted to bytes. If no data is copied in these conversions, then I'd rather have the payload be delivered as bytes to keep things compatible with the current version.

@Mitch-Martinez
Copy link
Author

Data is copied in these conversions, doing the following shows it has a different id, which should mean that it's in a different place in memory.

>>> a = b'somebytedata'
>>> b = bytearray(a)
>>> id(a)
140171735861408
>>> id(b)
140171735743728

So after knowing that a copy is taking place I tested using timeit
running timeit on the bytearry(some_bytes) operation takes a few hundred nanoseconds. I used 4096 because that should be the default max message size.

>>> timeit.timeit("b = bytearray(a)", setup="a=b'0'*4096", number=100000)/100000
3.420370000003459e-07

Would this be expensive for clients/servers that are sending a ton of small packets at a given time?

I've been focusing on doing large transfers of data and not paying too much attention to the efficiency of small packets.

Just out of curiosity I did a similar test with 1MB of data and saw that it takes about a 1/10th of a millisecond. Doing this type of coversion only once is a very small price compared to the many copy operations being done currently, but that can only be said for large messages at the moment.

>>> timeit.timeit("b = bytearray(a)", setup="a=b'0'*1048576", number=100000)/100000
8.481695999999829e-05

So without any further testing would it make sense to generalize that this change would add about 350 nanoseconds per message but it would start to save more time if messages get much larger (really in the MB range is when it is visually noticable on the command line). But when messages do get past 10MB the receiver slows down significantly when it should only take a few tens to hundreds of milliseconds to transfer depending on network speed.

I'll probably take some time to test out the time differences with and without the changes to be a little more thorough. It might make sense to only do these conversions if the incoming message has to be reassembled.

@miguelgrinberg
Copy link
Owner

miguelgrinberg commented Jul 3, 2022

Thanks, this was a useful discussion. I ended up implementing a slightly different solution that switches both text and byte messages to bytearray for faster appending, but only on arrival of the second part. So single part messages, which are the most common, do not suffer any performance penalties. Multipart messages are then converted back to str or bytes once the final part is received.

Could you please give the main branch a try, and let me know if this is satisfactory for your use case?

@Mitch-Martinez Mitch-Martinez deleted the increase-receive-speed branch July 12, 2022 17:05
@Mitch-Martinez
Copy link
Author

Sorry for the late reply, just got back from a short vacation.

These changes do work for me when using simple-websockets. How can these changes be reflected in the flask-sock package? This was how I was originally turned onto simple-websockets.

@miguelgrinberg
Copy link
Owner

@Mitch-Martinez This fix is now release. Upgrade simple-websocket and you'll have access to this from Flask-Sock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants