Skip to content

[Blazor][Fixes #11964] Limit the amount of pending renders #12763

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

Merged
merged 11 commits into from
Aug 6, 2019

Conversation

javiercn
Copy link
Member

  • Adds a limit to the number of unacknowledged pending batches.
  • After a client reaches that limit we stop procesing render requests from the components.
  • We still process events, but we don't update the UI.
  • When a client is not able to ACK renders as fast as the server produces them, the result is that the server slows down to the clients pace.
  • When a client stops acknowledging renders all together the server simply stops producing new UI updates.
  • The max memory the server will use in this situation is the size of the queued pending batches + the size of the queue of components to render.
    • Components extending ComponentBase will not try to queue more than 1 render request at a time.
    • Components implementing IComponent directly are responsible for not queuing successive more than one render request at a time.

@@ -423,6 +432,7 @@ private void ProcessRenderQueue()
{
// Ensure we catch errors while running the render functions of the components.
HandleException(e);
return;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug. After an exception we should stop everything immediately and not run anything else, like the code after this try..catch..finally block.

@javiercn javiercn requested a review from rynowak July 31, 2019 16:02
@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Jul 31, 2019
// Invke ProcessBufferedRenderRequests so that we might produce any additional batch that is
// missing.
// Its also safe to use the discard as ProcessRenderQueue won't throw.
_ = Dispatcher.InvokeAsync(() => ProcessRenderQueue());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to dispatch here? None of the above logic is thread-safe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically you either have the sync context or you don't 👍

  • If you do InvokeAsync should just run the code inline
  • If you don't then a lot of this code seems unsafe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree no need to use InvokeAsync.

Also, calling ProcessRenderQueue does a bunch of work that is almost always unnecessary in this case. Can I suggest you put a guard clause at the top of ProcessRenderQueue so it just returns if _batchBuilder.ComponentRenderQueue.Count == 0? Then we can also remove the existing check for that condition from the bottom of ProcessRenderQueue itself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to dispatch here? None of the above logic is thread-safe.

Component renders must run inside the sync context, as components require a sync context when they add to the render queue, which happens as part of processing the queue.
We also rely on the sync context to serialize render processing (meaning only 1 thread at a time operates on the queue and produces a new render). That's why its a queue and not a concurrent queue and why we don't have Interlocked.Exchange all over the place.

  • If you don't then a lot of this code seems unsafe.

Acknowledging the batch can happen outside of the sync context because of the comment mentioned above regarding signalr dispatching only 1 message a time and because the OnAfterRenderAsync calls get queued inside the sync context within ProcessRenderQueue when the UpdateDisplayAsync task doesn't complete synchronously.

Also, calling ProcessRenderQueue does a bunch of work that is almost always unnecessary in this case. Can I suggest you put a guard clause at the top of ProcessRenderQueue so it just returns if _batchBuilder.ComponentRenderQueue.Count == 0? Then we can also remove the existing check for that condition from the bottom of ProcessRenderQueue itself.

_batchBuilder is a private member of the base class and i think it's worth exposing it. We can afford having an extra comparison on the base class, even if its not super common, I don't think it affects perf. An alternative would be to add a HasPendingRenders method/property on Renderer or a new method on Renderer that wraps the call to ProcessRenderQueue with that check.

But I don't think either of those are a better solution for simply avoiding a check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledging the batch can happen outside of the sync context because of the comment mentioned above regarding signalr dispatching only 1 message a time and because the OnAfterRenderAsync calls get queued inside the sync context within ProcessRenderQueue when the UpdateDisplayAsync task doesn't complete synchronously.

Right, I see. This has confused us multiple times now, so maybe we should reconsider relying on that. If your change here means we definitely have to do an InvokeAsync for the ProcessRenderQueue call, could we instead force the entire execution of OnRenderCompleted to run on the sync context? That would more closely match our intuitions, and only results in the same amount of dispatching. It's not like we're really losing parallelism by doing this, since as you note, SignalR serializes the calls anyway.

_batchBuilder is a private member of the base class and i think it's worth exposing it

Sorry if I was unclear. I meant putting the guard clause at the top of ProcessRenderQueue in the base class, which already has access to _batchBuilder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I was unclear. I meant putting the guard clause at the top of ProcessRenderQueue in the base class, which already has access to _batchBuilder.

I think I addressed this?

Copy link
Member

@rynowak rynowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking a lot simpler than the earlier iteration 👍

@@ -8,6 +8,7 @@
using System.Threading;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really great that you got this new behavior into RemoteRenderer and not in the Renderer base class 👍

@javiercn javiercn force-pushed the javiercn/limit-render-queue branch 2 times, most recently from 05db8a7 to a9c90fe Compare August 5, 2019 14:09
@javiercn
Copy link
Member Author

javiercn commented Aug 5, 2019

🆙 📅

@SteveSandersonMS Maybe we need to have a chat to hash out the public API for this, I think I gave you good arguments for keeping it as it is (with the addition that I added) but if you don't feel that way, lets talk about it.

@javiercn
Copy link
Member Author

javiercn commented Aug 5, 2019

🆙 📅

@BrennanConroy
Copy link
Member

Commits don't look right

@javiercn javiercn force-pushed the javiercn/limit-render-queue branch from 4091732 to cc8d375 Compare August 5, 2019 16:00
@javiercn javiercn force-pushed the javiercn/limit-render-queue branch from 7f3df75 to 9ea2803 Compare August 5, 2019 16:09
@javiercn
Copy link
Member Author

javiercn commented Aug 5, 2019

Commits don't look right

I fixed them.

private void ProcessRenderQueue()
{
EnsureSynchronizationContext();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can go into ProcessPendingRender, since that's the external entrypoint, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it couldn't because it was on the virtual method, but not that I think again it can, as people is forced to invoke the base to do any meaningful work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change this in a separate PR as I don't want to re-run the build

Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! This is a complex area but this PR gives me confidence we're handling it well.

@javiercn javiercn merged commit 31cfa2e into release/3.0 Aug 6, 2019
@ghost ghost deleted the javiercn/limit-render-queue branch August 6, 2019 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants