-
-
Notifications
You must be signed in to change notification settings - Fork 22
Threaded dispatcher loop #23
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
Threaded dispatcher loop #23
Conversation
It's a little tricky to do this since we need a good method of exiting the I was investigating wrappers for |
This reverts commit 9ff7372.
Putting the dispatch into a My research shows that |
|
After some testing, |
I'm wondering if the threading belongs inside of IDOM core's Would allow us to use the more performant queue up in Django-IDOM, and avoid the mess of running a full async coroutine inside of a thread. |
You need to thread the whole dispatch loop because each render is inherently sequential. I'm starting to wonder whether this is premature optimization. Perhaps it would be best to wait on this until there's a demonstrable need for it. |
Being limited to half a page load per second for a relatively simple web layout is pretty rough. Would definitely limit Django-IDOM adoption. |
This doesn't quite make sense to me - rendering the page with IDOM should be a CPU limited task so threading shouldn't make the page load any faster. Maybe we should meet to discuss these results? |
Well in the case of web modules that is IO bound. And any sync code completely demolishes Django's Websocket Consumer event loop. We can schedule some time to discuss. Weekdays are going to be rough for me, usually only free after 8. But I should have better availability on weekends. |
I could meet after 8 at some point this week if you're able. I think I just want to understand exactly what's happening so I can come up with the best solution to this. |
Based on this gist I've determined I'm doing the whole asyncio-in-thread thing wrong. Will rewrite this PR to do it properly sometime this week. I currently don't have any plans for after 8 M-Th, so feel free to throw something down on my calendar to discuss. |
I'm not sure if this threading implementation will ever get merged, mostly because thread safe queue performance is awful. First paint times are improved a lot by this, but IDOM component updates take a huge hit. I believe the current performance boost comes from being able to not block the WS event loop, which allows for rendering during IO operations such as Websocket send/receive and file read/write. I've tried writing an async wrapper for My last shot is going on be funneling all queue operations through a dedicated |
I wonder if this problem could be eased by introducing some utilities to make rendering some parts of the UI in a thread easier. For example, maybe introducing a @component
def MyComponent():
view = use_thread(MainView, FallbackView)
... that could be used in a higher-level decorator: @component
@threaded(FallbackView)
def MainView():
... |
I'm not sure if that could solve the situation. If We'd either need to write things in a way that doesn't rely on |
Ok, so the threading solution would have some limitations I'm realizing (in short the decorator idea wouldn't work) but it could still work for some things. Basically def use_thread(default, function):
state, set_state = use_state(default)
@use_effect
async def run_in_thread():
loop = asyncio.get_event_loop()
queue = asyncio.Queue()
Thread(target=lambda: loop.call_soon_threadsafe(queue.put, function()), daemon=True)
set_state(await queue.get())
return state Usage would look like: @component
def MyComponent():
value = use_thread(default=None, function=compute_expensive_value)
if value is None:
return FallbackView()
else:
return TheActualView(value) Initially we'd display the |
Another way to improve this situation could be to simply yield control back to the event loop more frequently while rendering. Presently, when the layout begins a rendering task it does not yield control back to the event loop until the update is complete. Would could however, make it yield back after the render of each component completes. This would prevent the server from locking up once a render starts. You'll note that this call within |
I think we should handle performance instances like this within the IDOM framework as best we can, rather than putting the burden on the developer to optimize. So the hook isn't something that I'd prefer. The options I would consider are:
|
I'm going to put this PR on hiatus to focus on Conreq Core. Trying to get a release out by the end of December. |
Just an idea... A fix for this could also be done upstream in django/channels. It looks like the channels team expects async websockets to have no sync code within their methods, Perhaps WS receive/send execution belongs in a thread? Django 4.0 comes out December 6th and it makes improvements to the request handler by running each independent request within it's own thread. I don't know if that extends to the websocket request handler though, so I suppose I'll wait and see. |
New implementation avoids Janus. Utilizes a backhaul thread (running an asyncio loop) to execute Unfortunately same results as the previous implementation. Vastly improves concurrency, but adds latency to layout updates. |
Given that this is a trade-off, can we add an option which would allow someone to choose what they want to do? Maybe |
I'd say yes depending on how things turn out on options two and three. I'm going to put together a quick test to measure the performance gains via this implementation. Simplest form would be rendering 100+ components on the page and comparing the threaded and non-threaded implementations. Side Note (pure speculation)It seems like the the first layout render ( Is there a significant amount of sync code that runs on the first layout paint? Could it be possible to asyncifiy some parts of the first paint stack, or thread parts that need to be sync? I might need to profile the dispatch loop using Yappi to dig deeper on this. |
Test Case <h1>IDOM Test Page</h1>
<!-- repeat_val = range(25) -->
{% for x in repeat_val %}
<div>{% idom_component "test_app.components.HelloWorld" class="hello-world" %}</div>
<div>{% idom_component "test_app.components.Button" class="button" %}</div>
<div>{% idom_component "test_app.components.ParametrizedComponent" class="parametarized-component" x=123 y=456 %}</div>
<div>{% idom_component "test_app.components.SimpleBarChart" class="simple-bar-chart" %}</div>
{% endfor %} Results
AnalysisWith a performance gain of only 11% on first paint, I'm going to close off this PR. The results are more noticeable at smaller quantities (as seen in the previous test), but bulk testing is more important in this scenario. Perhaps it's the first WS handshake with the server that is expensive? Not worth adding noticeable latency on subsequent layout updates for trivial performance improvements. These results also tell us that Django-IDOM can first paint roughly 18 components per second (using the development webserver). |
Thanks for looking into this! |
This PR attempts runs the dispatcher in a thread.
Merging this pull request will significantly increase scalability. For example, would allow servicing requests in scenarios where there's hundreds of clients connected to a single webserver.
Details
self._idom_dispatcher_thread
asyncio.Queue
with a thread-safe async queuejanus.Queue().async_q
super()
indisconnect()