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

Ensure ComponentState is always disposed #48406

Merged
merged 3 commits into from
May 24, 2023

Conversation

SteveSandersonMS
Copy link
Member

Addresses an issue where RenderTreeFrame[] pooling became ineffective because ComponentState instances were only disposed if the component itself was IDisposable (and not IAsyncDisposable).

The approach is:

  • In Renderer, stop varying the code path based on different disposability patterns for the component. Just call componentState.DisposeAsync in all cases.
  • componentState.DisposeAsync unconditionally releases the buffers before looking at the disposability pattern for the component and calling Dispose or DisposeAsync as appropriate.

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label May 24, 2023
@@ -34,7 +35,6 @@ Microsoft.AspNetCore.Components.Rendering.ComponentState
Microsoft.AspNetCore.Components.Rendering.ComponentState.Component.get -> Microsoft.AspNetCore.Components.IComponent!
Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentId.get -> int
Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentState(Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer, int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> void
Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose() -> void
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a breaking change because ComponentState is only becoming public in 8.0.

Copy link
Member

Choose a reason for hiding this comment

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

I see.

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented May 24, 2023

I looked into ways this could be validated in tests or even at runtime normally, but didn't find a great option:

  • If we track the rent/return counts inside ArrayBuilder instances, we can only assert it's gone to zero when something is disposing the ArrayBuilder, which is pointless because that's the case that's always going to work. What we want to detect is cases where it does not get disposed.
  • We can't track/check it statically, because then unrelated Renderer instances would interfere

We would need to do something like treat the renderer as the boundary for a context in which ArrayBuilder can be instantiated, so they all register themselves with the owning renderer. Then when the renderer is disposed it could look at all the associated ArrayBuilder instances to check they have been disposed. However that involves a lot more tracking, and we don't generally put in these heavy guardrails everywhere to verify things get disposed, so it doesn't seem like the right thing to do.

We do have unit tests already that show component instances do get disposed during renderer disposal. Since this PR changes the disposal logic so that components are only disposed through disposal of their ComponentState, we do effectively have at least that level of coverage.

@javiercn
Copy link
Member

We do have unit tests already that show component instances do get disposed during renderer disposal. Since this PR changes the disposal logic so that components are only disposed through disposal of their ComponentState, we do effectively have at least that level of coverage.

Could we add some tracking logic on our TestRenderer to know when a ComponentState instance gets created and when it gets disposed?

That should give us a way to track this in tests, isn't it?

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

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

Looks good, just one comment about the tests

@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review May 24, 2023 13:25
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner May 24, 2023 13:25
@SteveSandersonMS
Copy link
Member Author

Could we add some tracking logic on our TestRenderer to know when a ComponentState instance gets created and when it gets disposed?

It's a good idea, so I've done it.

However this has led to some further changes, so please re-review:

  • I've had to make ComponentState's DisposeAsync virtual, as otherwise the tests can't observe the disposal. At first I was going to decline to add the tests on this basis, but on more consideration, it makes sense that if a renderer can subclass ComponentState, it may have reasons to want to do things when those instances are disposed. I'm trusting that the overhead of virtual dispatch isn't noticeably expensive here, or that if it is, it will show up in the benchmarks (particularly for WebAssembly).
  • This means we now must call ComponentState's DisposeAsync in all cases. Previously we didn't call it when disposing into a RenderBatch, as that was a separate code path and independently dealt with clean up everything (correctly as far as I can tell). However that would now violate the expectations of anyone subclassing ComponentState and overriding DisposeAsync. So I've had to change the dispose-during-renderbatch code paths.
    • Previously there was both TryDisposeInBatch and DisposeInBatchAsync implemented independently. I've merged these into a single DisposeInBatchAsync, and removed most of its guts so it just calls DisposeAsync instead of doing its own cleanup.

I think the end result is much clearer - this collapses about 6 different disposal code paths into just two (during a renderbatch, and during renderer teardown). The only drawback is that DisposeAsync is now virtual but there's a good argument for that being more correct anyway.

@javiercn
Copy link
Member

I think the virtual call on ComponentState is fine. Hopefully the JIT is able to see that there is only one implementation and devirtualize the calls.

@SteveSandersonMS SteveSandersonMS enabled auto-merge (squash) May 24, 2023 14:54
@SteveSandersonMS SteveSandersonMS merged commit 7e3dcf7 into main May 24, 2023
@SteveSandersonMS SteveSandersonMS deleted the stevesa/fix-componentstate-disposal branch May 24, 2023 15:20
@ghost ghost added this to the 8.0-preview6 milestone May 24, 2023
@SteveSandersonMS
Copy link
Member Author

/backport to release/8.0-preview5

@github-actions
Copy link
Contributor

Started backporting to release/8.0-preview5: https://github.com/dotnet/aspnetcore/actions/runs/5070919992

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.

3 participants