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

[Blazor] Strange update behavior #15175

Closed
arivoir opened this issue Oct 18, 2019 · 12 comments
Closed

[Blazor] Strange update behavior #15175

arivoir opened this issue Oct 18, 2019 · 12 comments
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. question

Comments

@arivoir
Copy link

arivoir commented Oct 18, 2019

I have an issue and I' trying to understand how Blazor updates work.

I have a component that has a nested component, a lot of them actually.

At some point I call the StateHasChanged of the parent, and a property of one of the children is set.

C1.Blazor.Input.dll!C1.Blazor.Input.C1CheckBox.IsChecked.set(bool? value) Line 199	C#
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Reflection.MemberAssignment.PropertySetter<C1.Blazor.Input.C1CheckBox, bool?>.SetValue(object target, object value)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Reflection.ComponentProperties.SetProperties.__SetProperty|2_0(object target, Microsoft.AspNetCore.Components.Reflection.IPropertySetter writer, string parameterName, object value)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Reflection.ComponentProperties.SetProperties(Microsoft.AspNetCore.Components.ParameterView parameters, object target)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(Microsoft.AspNetCore.Components.ParameterView parameters)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.UpdateRetainedChildComponent(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldComponentIndex, int newComponentIndex)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldFrameIndex, int newFrameIndex)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldFrameIndex, int newFrameIndex)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldFrameIndex, int newFrameIndex)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldFrameIndex, int newFrameIndex)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(ref Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Microsoft.AspNetCore.Components.RenderTree.Renderer renderer, Microsoft.AspNetCore.Components.Rendering.RenderBatchBuilder batchBuilder, int componentId, Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> oldTree, Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> newTree)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(Microsoft.AspNetCore.Components.Rendering.RenderBatchBuilder batchBuilder, Microsoft.AspNetCore.Components.RenderFragment renderFragment)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(Microsoft.AspNetCore.Components.Rendering.RenderQueueEntry renderQueueEntry)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()	Unknown
Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer.ProcessPendingRender()	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment)	Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()	Unknown

What is that supposed to be?

Neither the BuildRenderTree of the parent nor the children were performed yet. Therefore the components weren't passed the new parameters yet.

We really need documentation about the rendering. It is extremely complicated to guess what will happen when calling StateHasChanged.

@javiercn javiercn added the area-blazor Includes: Blazor, Razor Components label Oct 21, 2019
@javiercn
Copy link
Member

@arivoir Thanks for contacting us.

We're not able to understand your specific scenario/question without a bit more info. Could you provide a link to a github repo with a minimal repro project that showcases your issue?

From what I can tell, seems like your parent component is triggering a render and as a result of that render the property in your checkbox is being set, from what I follow it happens in this line

Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters)	Unknown

So the flow likely is:

  • Parent component calls StateHasChanged
  • A new render tree is produced
  • The diff between the old tree and the new tree is happening
  • The values being passed to your child component are considered different to the ones it currently hold.
  • SetParameters is being called on the child component to update them with the values the parent passed to it.

@javiercn javiercn added Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. question labels Oct 21, 2019
@javiercn
Copy link
Member

If the answer helps you, feel free to skip the repro and close the issue.

@arivoir
Copy link
Author

arivoir commented Oct 21, 2019

@javiercn Thanks for your answer. Unfortunately, it is hard for me to reproduce this in an isolated project. Maybe I have time in a future.

You said the order is the following

  1. Parent component calls StateHasChanged
  2. A new render tree is produced
  3. The diff between the old tree and the new tree is happening
  4. The values being passed to your child component are considered different to the ones it currently hold.
  5. SetParameters is being called on the child component to update them with the values the parent passed to it.

Thanks for this, is the first hint of documentation I found regarding rendering. I really appreciated.

This trigger some more doubts.

You said in number 2 "A new render tree is produced". Are you referring to the tree of the specific component whose state changed or the whole tree? Clarifying/Documenting about the "Invalidation" mechanism is appreciated.

In number 5 you said the SetParameters of the child is called. If 4 determines there are no changes in the child, does 5 happen? if 5 doesn't happen, is the children BuildRenderTree executed? Can it happen that the BuildRenderTree of a parent is executed, but not the children one?

@javiercn
Copy link
Member

@arivoir

Your call stack is around here
https://github.com/aspnet/AspNetCore/blob/master/src/Components/Components/src/Rendering/ComponentState.cs#L64-L74

  • When you call StateHasChanged, it simply queues a render request for that component.
  • If no current render batch is in process we start producing a new render batch, which batches all the components that want to be re-rendered at the same time (we have a queue and we process it until its empty, that's what we call a batch).
  • When we start rendering a component (let's say your parent component).
  • We call BuildRenderTree, we store the results and we compare them with the previous render tree from the previous version. (That's the diffing process).
  • Components render independently, so if you have child components their render trees are separate from the parent render tree. The parent render tree only holds a reference to the child component id and the parameters it is passing to it.
  • As part of the diffing process, the parent component determines what to do with their children:
    • If they are not part of the new tree it removes them.
    • If they are in the same position but have different attributes applied (it determines this by comparing the attributes in the old tree with the attributes in the new tree) it creates a new ParametersView with the new set of attributes and calls SetParametersAsync on the child component (which will cause the child component to call StateHasChanged and queue a new render, which will then be processed inside the same batch).
    • If they are in the same position and have the same attributes applied it does not pass in new parameters to the child component (which in turn triggers no render). (This is determined by the notion of equality we use for comparing the attribute lists).

The algorithm for determining this is quite conservative, but at most it would results in an additional render. It checks the attributes in the order they are declared and determines they are different in 3 situations.
* If they have a different type.
* If they are not know immutable objects (short, int, bool, decimal, Type, etc)
* If their value is different according to oldValue.Equals(newValue).

The order part is important, but not significant. It improves the performance of this area a lot, (which is critical) and you won't run into it unless you are doing something very sophisticated that renders the same set of attributes in different order.

Finally, a way to debug these types of things is to override SetParametersAsync in your components, call base and use breakpoints to trace the flow of events.

@mkArtakMSFT
Copy link
Member

Thanks for contacting us, @arivoir.
We will be covering the missing docs as part of #14591

@mkArtakMSFT mkArtakMSFT added the ✔️ Resolution: Duplicate Resolved as a duplicate of another issue label Oct 21, 2019
@arivoir
Copy link
Author

arivoir commented Oct 21, 2019

@javiercn

I am much more clear about the relation between parent and children now. Thanks you for that. I will continue thinking on this and try to make my mind around in different scenarios.

One related thing it really concerns to me is the StateHasChanged ends up calling the BuildRenderTree synchronously. this is really problematic because the components could end up making a lot of rendering repeatedly. For example, in a component that display a list of items, if a customer writes a loop adding data items, the component has no control of how many items will be added, and it calls StateHasChanged for each item. if this method triggers the rendering synchronously, it will perform a rendering that will finally be discarded. To resolve this problem, many ui platforms (All Xaml, Android, iOS) implement an "Invalidation" pattern. This pattern consist the StateHasChanged only "marks" a component as dirty and the actual rendering is performed asynchronously. This way, calling the StateHasChanged is really cheap, can be called multiple times, and the rendering is performed only one time.

@javiercn
Copy link
Member

@arivoir

I’m sorry if it wasn’t clear. That’s more or less how it works. Components simply request a render, the tenderer then decides when to render them.

For example, for event handlers we produce a single render batch for the entire event and normally each component gets rendered at most once (when a component has already a pending render queued it doesn’t queue another one, you can see this in componentbase.cs)

We have a pending doc issue to do a write-up for this.

@arivoir
Copy link
Author

arivoir commented Oct 21, 2019

@javiercn

Components simply request a render, the tenderer then decides when to render them.

The point is it shouldn't call the render synchronously ever.

In one of the components, I'm seeing the BuildRenderTree is called directly

C1.Blazor.Grid.dll!C1.Blazor.Grid.GridCellsPanel.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) Line 349 C#
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase..ctor.AnonymousMethod__6_0(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(Microsoft.AspNetCore.Components.Rendering.RenderBatchBuilder batchBuilder, Microsoft.AspNetCore.Components.RenderFragment renderFragment) Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(Microsoft.AspNetCore.Components.Rendering.RenderQueueEntry renderQueueEntry) Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue() Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender() Unknown
Microsoft.AspNetCore.Components.Server.dll!Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer.ProcessPendingRender() Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) Unknown
Microsoft.AspNetCore.Components.dll!Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged() Unknown
C1.Blazor.Grid.dll!C1.Blazor.Grid.GridCellsPanel.Refresh(C1.Blazor.Grid.GridCellsPanelRange intersection) Line 507 C#

If I call StateHasChanged multiple times it calls BuildRenderTree multiple times too. Do you think this is normal?

@javiercn
Copy link
Member

If I call StateHasChanged multiple times it calls BuildRenderTree multiple times too. Do you think this is normal?

That is not the case https://github.com/aspnet/AspNetCore/blob/master/src/Components/Components/src/ComponentBase.cs#L102-L119

https://github.com/aspnet/AspNetCore/blob/master/src/Components/Components/src/ComponentBase.cs#L40-L44

Adding a call to StateHasChanged simply queues the component to be rendered. The renderer decides when the renders happen. This can be triggered by 4 circumstances:

  • Initial render where the bootstrap process triggers the initial render of the root component and all its children.
  • An event, in which the component that handles the event automatically triggers a new render after the event, and potentially its children if it renders new children or change their parameters.
  • As a result of calling StateHasChanged from an InvokeAsync call (marshalling back into the UI thread, essentially)
  • As a result of the parent component changing the parameters for the child component, which happens as part of the diffing process when the renderer calls SetParametersAsync on the child component.

To be very clear, calling StateHasChanged only queues a Render for the component or "marks it as dirty".

It's the renderer the one that decides when and how to produce the renders.
BuildRenderTree does not result in new rendered output, only in a new definition of the "V-DOM" for the component at the time it's being called.
Normally, a component gets rendered once per render batch (which is a collection of components that are rendered/diffed together and sent to the UI for update). There are only two situations in which a component renders more than once in a batch:

  • You have a component that directly implements IComponent and calls RenderHandle.Render(https://github.com/aspnet/AspNetCore/blob/master/src/Components/Components/src/RenderHandle.cs#L50) directly without checking if it already queued a pending render (not common to implement IComponent yourself and the recommendation is to follow the approach in ComponentBase, where you don't queue multiple renders)
  • You have a circular dependency between a child and a parent component that might cause a parent to re-render as part of a children invoking some callback parameter from the parent as part of its initialization.

I don't want to turn this issue into a discussion as we plan to provide a doc explaining in detail how it works and I think its better if we postpone further discussion until we have a coherent document where everything is explained properly instead of as a collection of ad-hoc questions/comments/answers.

Hope this helps clarify things a bit further. If you want to dig in a bit more, I would recommend to create a simple sample and debug through the framework if you want to better understand how the rendering process works. It should be approachable if you use sourcelink, that's what we internally do.

@arivoir
Copy link
Author

arivoir commented Nov 5, 2019

@javiercn Thanks for providing this information.

I want to make some emphasis in 2 points

  1. Despite multiple calls to the same BuildRenderTree could end up in just 1 render, this can cause performance trouble if executing BuildRenderTree is complex. It is not enough to avoid "renders" it should be also possible to skip "BuildRenderTree"'s.

  2. The fact that the BuildRenderTree is sometimes called synchronously and sometimes asynchronously causes trouble to update the internal state of the components when a determinate action needs to "invalidate" more than one children.

IMO, the calls to "BuildRendeTree" should be always asynchronous, and they should be skipped if are repeated.

@arivoir
Copy link
Author

arivoir commented Nov 5, 2019

@javiercn I just found that there are not only problems in the performance. Let me explain my case a little more.

I have a component (data-grid) that has 4 panel of cells, each one is a component which renders a grid of cells. In some cases it is necessary to change the range of displayed cells in all the panels, for example if you change the source of the grid.

The problem I'm facing is when I call StateHasChanged in one of the "panels", it ends up calling the "BuildRenderTree" of the sibling panel, whose model haven't been updated yet. This is really bizarre. Can you say why happens this?

@Liander
Copy link

Liander commented Nov 18, 2019

@arivoir Look if it is linked to having a delegate as a parameter. I don't know if it relevant in your situation but I mention it because it can cause side-effects on the rendering which is not so obvious. See #13610 (comment).

@javiercn It is easy to think that passing a delegate is just an initialization which will not involve the conditions for rerendering and it is never described. Is this something you are planning to look into? I have asked it a few times before on different tasks but never recieved an answer.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. question
Projects
None yet
Development

No branches or pull requests

4 participants