-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
MudPopover: Fixes leaks caused by concurrency between OnAfterRenderAsync and DisposeAsync #3963
Conversation
Codecov Report
@@ Coverage Diff @@
## dev #3963 +/- ##
=======================================
Coverage 91.12% 91.12%
=======================================
Files 351 351
Lines 11603 11612 +9
=======================================
+ Hits 10573 10582 +9
Misses 1030 1030
Continue to review full report at Codecov.
|
|
||
public Guid Id { get; init; } | ||
public Guid Id { get; private set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove init? The Id is only set once in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was no code actually using the init setter and there's no reason for it to set be set outside the ctor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And isn't that the reason to have init
instead of private
? To make sure this property is only set in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair, the correct solution here would be:
public Guid Id { get; }
No setter means it can only be initialized in the constructor. A private set
allows the property to be set from within the class. init
however is a public setter that can only be used either in the constructor OR in the initialization block, the latter being a publicly usage which we really don't want.
I will amend the PR!
_detached = true; | ||
|
||
if (IsConnected) | ||
await _runtime.InvokeVoidAsync("mudPopover.disconnect", Id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please wrap the call inside { }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries! Done.
Hi @diegofrata and thanks for your work. For some reason, I've missed the concurrent scenario in this case. Other components and handlers do use a |
9ba9362
to
314b614
Compare
To be honest it's the first time I run into this behavior with Blazor. I am not entirely sure I agree with it, I don't think Dispose should ever run concurrently to OnAfterRenderAsync. I think this needs to be raised further with the Blazor devs, I need to find the appropriate channel for doing so. It is a massive gotcha. Thanks for reviewing it! |
I had encountered other strange behavior regarding the execution syntax. If you want to see exciting effects, investigate the difference between awaiting the execution of an |
…DisposeAsync. Add missing braces to if-condition Patch flaky test and remove setter from Id
314b614
to
25b3193
Compare
@just-the-benno I have noticed the tests I wrote were flaky, there's no good way to test that the locking I introduced works correctly other than adding a small delay OR abstracting the entire semaphore/locking and then mocking it. I opted for a 50ms hit in the test suite here, as the latter would be a lot more work. |
I understand your perspective, plus the abstraction is more work and more code to maintain without a "functional" benefit. I think these concurrent situations are the "limit" of what can and should be tested within the scope of unit testing. I struggled my self in other components, too. The only thing I would try (not sure if it works) is to render multiple popovers simultaneously and look at the js calls. If such a test passes, you go back with your source code at the start and see if the test is failing. That is my approach to those problems:
But that said, for this situation, in particular, It might not even be possible to provoke the error in the first place. |
@just-the-benno the semaphore is per popover in this case, not global, so I don't think the suggestion can be applied! I am confident the test demonstrates the fix though. For future work, I think components that rely on interop should inherit from a special base class that overrides and seals both OnAfterRenderAsync and DiposeAsync and expose virtual methods that are already wrapped with a semaphore. Are you happy to merge as is? I am quite keen on having this released soon to improve the performance of my app, currently grinding to a halt after a number of renders (lots of tooltips!). |
Yes, per handler, that slipped my mind while writing the text above. While not opposing the idea of a base class, keep in mind that there are different types of interop calls. Some are stateless, like I need to investigate why a non-related test is failing. And as soon as it is solved, we can merge it. |
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
I don't know for sure if it's applicable here, but the blazor team recommends using a js |
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Fixes memory leak caused by concurrent calls of AfterRenderAsync and DisposeAsync. (MudBlazor#3963)
Description
Attempts fixing #3961 by controlling the concurrency with a semaphore slim.
In certain situations, Blazor will call DisposeAsync while OnAfterRenderAsync is still being executed. Apparently, this is by design and when it happens to a MudPopover component, it leads to the handler being detached first AND then initialized -- leaving the JS objects in the browser and slowing down the application over time.
This PR addresses two scenarios:
Calls are made concurrently and reach Initialize first and then Detach on MudPopoverHandler
In this case, everything works as expected as the semaphore will prevent Detach from proceeding until Initialise finishes.
Call are made concurrently and reach Detach first and then Initialise
In this case, the MudPopoverHandler will be marked as being detached and Initialize will be a no-op.
It also aims to improve overall performance by replacing MudPopoverService list of handlers with a dictionary.
How Has This Been Tested?
Types of changes
Checklist:
dev
).