-
-
Notifications
You must be signed in to change notification settings - Fork 328
What is the intended value of concurrent rendering? #1203
-
Is someone able to clarify why effort was made to create this feature? Is there a test case that demonstrates its value?
I am asking because I have a pretty complex object that takes tangible time to render, and I was unable to notice a difference. When I dug through the code, I am not fully certain, but from what I can tell, the concurrent render provides no advantages over the serial one.
The reasoning is that components shouldn't have blocking I/O anyways, and you can't use an await in them, so all of your components will kicking off data fetching via an async use_effect anyways.
If the intent was to leverage multiple CPUs, then the feature needs to be rewritten to leverage that, unless there's some Python 3.12 / 3.13 feature that makes this more worthwhile (and there might be, I haven't dug into 3.12 recently)
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments 6 replies
-
Rationale is in the PR
#1165
Beta Was this translation helpful? Give feedback.
All reactions
-
Based on the PR not really elaborating, it gives me the impression that there was an assumption that giving asyncio 5 tasks to execute would result in in them being executed concurrently, like an OS scheduler, rather than it executing the first task to completion or until it hits a blocking call, then moving on to the next, until it completes a task. The issue I'm seeing is that component logic is (currently) all non-async so there aren't going to be any blocking calls.
You could artificially add blocking calls (for example, asyncio.sleep(0)) to allow smaller tasks to finish ahead of larger ones, but it would incur the performance penalty of the context switching and possibly be slower in most circumstances. And actually, since the component logic isn't inside an async function, it's not really clear how you would do that.
On top of that, your largest components are probably the root components, which need to be rendered first anyways.
This goes back to "is it worth it?" There's an overhead to maintaining multiple code paths, and the extra complexity will add to the overhead performance cost.
One alternative idea is to give components an optional priority, which would allow them to jump in front of the others. Then instead of a queue you use a priority queue to figure out what to render next. This could be done by changing the _rendering_queue to a queue.PriorityQueue (or I suppose it's an asyncio.PriorityQueue in this context) which I think should work without having to do any finagling.
Not trying to shoot down work people have done, but it makes sense to justify these things before further work is spent on it if its not clear whether there's value in the first place.
Beta Was this translation helpful? Give feedback.
All reactions
-
It doesn't assume concurrent rendering, which is why I requested Ryan to change the name to async rendering.
A single ASGI worker can service thousands of components, which similarly can have thousands of effects and events.
By asyncifying more things, it allows us to queue other asyncio tasks (events, hooks, and ASGI send/receive events) in-between render operations.
A simpler way of putting it is: ASGI webservers have better throughput during high loads the more async yielding you provide. This is even more beneficial for event loop implementations such as uvloop.
Beta Was this translation helpful? Give feedback.
All reactions
-
Is the intent to change the components to be async? Is there some magic I am missing that is making them async in some way?
async def _render_component( self, exit_stack: AsyncExitStack, old_state: _ModelState | None, new_state: _ModelState, component: ComponentType, ) -> None: ... raw_model = component.render() # no await, sync function
By asyncifying more things, it allows us to queue other asyncio tasks (events, hooks, and ASGI send/receive events) in-between render operations.
This is already happening with _serial_render
Beta Was this translation helpful? Give feedback.
All reactions
-
I haven't tested out the performance of Ryan's PR, but theoretically it should help throughput when dealing with multiple root components and lots of async effects.
Beta Was this translation helpful? Give feedback.
All reactions
-
Did some digging to investigate whether my priority queue suggestion would work and found that what's happening is that a lot of rendering is coupled together. Even moving some nested components to their own event didn't help, as it seemed like the top level element was being re-rendered so the priority on them was effectively ignored. The logic would need to be altered such that every component gets sent to the render queue, not just some components that get scheduled.
But it gets back to whether this is even needed. The actual render time is really small. If you want to speed things up, I would drop the runtime protocol attribute checks which show up a surprising amount on my performance trace. The protocols are annoying anyways because the IDE doesn't know how to go from ComponentType to Component. Just use a base class. The current use of protocols break find references in the IDE. Searching for Component.render shows up no usages. Searching for ComponentType.render shows no definitions other than the protocol.
Nearly 50% of the render time is inside an instance check.
instance-check
if not isinstance(root, ComponentType): msg = f"Expected a ComponentType, not {type(root)!r}." raise TypeError(msg)
Beta Was this translation helpful? Give feedback.
All reactions
-
I do 100% agree that ditching the protocols is a good idea. I've generally been annoyed with them, and would prefer base classes.
Beta Was this translation helpful? Give feedback.
All reactions
-
Several instances of protocol checks have been replaced with a base class within #1281
Beta Was this translation helpful? Give feedback.