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

Performance Best Practices for Python are not complete, that makes people accept false assumptions (at least for me and our organization) #1015

Open
HardNorth opened this issue Aug 1, 2022 · 0 comments
Assignees

Comments

@HardNorth
Copy link

HardNorth commented Aug 1, 2022

Hello there! I would like to point on statements in Python paragraph of Performance Best Practices article. I can't say anything regarding to server-side implementation in Python language but we at Report Portal seeking for the right way to implement our gRPC clients on different languages. And through reading the paragraph these things are coming in mind regarding to the best client implementation:

  • Avoid spawning new threads
  • Use asyncio channel
  • Use basic asyncio implementation of the client on the only thread (Main Thread, achievable through asyncio.run)
  • Use unary RPC calls

I've made several performance tests with different approaches and can tell that if someone implement such a client it will be the worst-performance client among of all possible variants.

Here is my test repo: https://github.com/HardNorth/grpc-bidirectional-streaming-test

And Proto file: reportportal.proto

I implemented a dummy Java server which does nothing but randomly waits for 0-200 milliseconds and copy request UUID to response: ReportPortalReportingService.java

The very basic implementation of the client is to use everything in a straightforward way: main_async_vanilla_unary.py
It's clean code and it does 2 awaits one-by-one on start and finish item. Here is the result:

$ python main_async_vanilla_unary.py 
INFO:__main__:Finishing the test of 500 items. Took: 106.0901300907135 seconds
INFO:__main__:Total thread number: 1

Well, just 500 items in 1:46, it's not that many. I assume the issue here that awaits execute in a strict order, so there is no much difference with synchronous code.

OK let's experiment further. In the next version I replaced two sequential awaits with one await asyncio.gather(*coroutines) call, collecting all coroutines into a list preliminary: main_async_not_so_vanilla_unary.py

$ python main_async_not_so_vanilla_unary.py 
INFO:__main__:Finishing the test of 50000 items. Took: 113.26398301124573 seconds
INFO:__main__:Total thread number: 1

Looks way better, almost 100 times better. But what if we do that with async streams: main_async_stream.py

$ python main_async_stream.py 
INFO:__main__:Finishing the test of 50000 items. Took: 103.9231948852539 seconds
INFO:__main__:Total thread number: 1

Streams do even better. Let's go further, what if we move async code into a separate thread? We need it since all of the Python test frameworks call their plugins in a synchronous way, so we need to implement synchronous-asynchronous bridge: main_new_thread_async_stream.py

$ python main_new_thread_async_stream.py
INFO:__main__:Finishing the test of 50000 items. Took: 103.00598907470703 seconds
INFO:__main__:Total thread number: 2

Even better as you see, but a drawback of such approach is a very complicated code. Probably I would find a way to simplify it, but that also means this is the most time-expensive approach. Let's go further and throw away async code doing everything in different threads: main_threads_stream.py

$ python main_threads_stream.py 
INFO:__main__:Finishing the test of 50000 items. Took: 103.59917211532593 seconds
INFO:__main__:Total thread number: 3

Well, not that worse than the best option. Actually the number of threads during the run was 4, this thread handled gRPC channel but was destroyed to the moment of logging.

Conclusion

From my point of view straight async unary RPC client is the worse option one can implement. Playing around execution order gives better results, but streaming endpoints make it even better. So I can't say the documentation is reasonable, I would recommend updating it with more details.

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

No branches or pull requests

2 participants