Skip to content

Fix event during onafterrender #32017

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 8 commits into from
Apr 22, 2021

Conversation

SteveSandersonMS
Copy link
Member

Fixes #30070, which is another case of execution order inconsistency between Blazor Server and WebAssembly.

Rather than fixing it by putting in another special case, I again wanted to design this class of bug out of existence by bringing WebAssembly hosting more closely into alignment with Server hosting. I say "again" because that was also the approach in #31612, but evidently there are still cases where I didn't go far enough. So now in this PR I'm going significantly further still by taking this out of the renderer and making a general-purpose work queue for all JS-to-.NET calls.

The solution here now simultaneously solves all three of the WebAssembly-specific event ordering problems we've had, and makes it possible to remove a lot of tricky special-case stuff we were using before. Hopefully it also has no perf or compat drawbacks for developers. It looks like not much code but it took a lot of thinking to reduce it to this, especially around things like exception handling. It's still not easy to reason about so please do say so if you have any concerns about whether it's really correct.

@SteveSandersonMS SteveSandersonMS added area-blazor Includes: Blazor, Razor Components feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly labels Apr 21, 2021
@SteveSandersonMS SteveSandersonMS requested a review from a team April 21, 2021 15:18
@@ -138,90 +160,6 @@ protected override void HandleException(Exception exception)
}
}

/// <inheritdoc />
public override Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? eventFieldInfo, EventArgs eventArgs)
Copy link
Member

Choose a reason for hiding this comment

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

Can you help me understand what the consequences of removing this API are?

Copy link
Member Author

Choose a reason for hiding this comment

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

The API remains, it's just this override that is no longer needed, since the base class implementation is now sufficient.

/// at least an extra allocation and layer of try/catch per call, plus more work to schedule continuations
/// at the call site.
/// </remarks>
public static void Schedule<T>(T state, Action<T> callback)
Copy link
Member

@HaoK HaoK Apr 21, 2021

Choose a reason for hiding this comment

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

nit: should the method be aligned with the class? WebAssemblyCallScheduler / Schedule, or WebAssemblyCallQueue.Enqueue ?

Although since this is static, there's a bit of redundancy, maybe WebAssemblyDispatcher.Schedule?

Copy link
Member Author

Choose a reason for hiding this comment

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

TBH with this being internal I'd be inclined to leave it. Trying to communicate something like "run it maybe right away or maybe later" is hard to put into a single word. We already use the term "dispatcher" for something different so I was going for "call queue" as the most literal statement of what it is.

// For now, this minimal work queue is an internal detail of how the framework dispatches
// incoming JS->.NET calls and makes sure they get deferred if a renderbatch is in process.

internal static class WebAssemblyCallQueue
Copy link
Contributor

Choose a reason for hiding this comment

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

Super nit suggestion: You could consider making this a singleton to make unit testing this type easier.

// we have to delay the renderbatch acknowledgement until it gets to the front of that queue.
// This is for consistency with Blazor Server which queues all JS-to-.NET calls relative to each
// other, and because various bits of cleanup logic rely on this ordering.
var tcs = new TaskCompletionSource();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this specify RunContinuationsAsynchronously?

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'm not aware of any need for that here.

{
if (_isCallInProgress)
{
_pendingWork.Enqueue(() => callback(state));
Copy link
Contributor

Choose a reason for hiding this comment

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

If you wanted to avoid the allocation, you could make this an Action<object>. This way the caller can be aware of any allocations incurred due to boxing.

Copy link
Member Author

@SteveSandersonMS SteveSandersonMS Apr 21, 2021

Choose a reason for hiding this comment

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

I didn't spot a good way to do that without forcing an extra allocation if T would otherwise be a value type (e.g., in the synchronous execution case where the implementation here can avoid boxing). I guess technically there could be two overloads of this method, one if T:class and one for T:struct. Also it would force the caller to put a typecast inside their callback which is a bit awkward.

Since 99% of the time the call will be executed synchronously, I didn't think it worth making the code that much more to maintain and risk future inconsistencies.

Please let me know if you disagree or have ideas for how to avoid that drawback.

Copy link
Contributor

Choose a reason for hiding this comment

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

None in particular. I guess if we profile this and see additional allocations, we could devise something interesting.

// No need to await because we want the root components to be added in parallel (e.g.,
// not waiting for each other's OnInitializedAsync tasks), and any async exceptions will
// be handled by normal component lifecycle mechanisms.
_ = renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters);
Copy link
Member

@javiercn javiercn Apr 22, 2021

Choose a reason for hiding this comment

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

This changes the semantics for line 175 below. At least we need to capture all of them and do a Task.WhenAll before we call store.ExistingState.Clear()

We should also look at what we do on Blazor server and Desktop and see if we do it in sequence or in parallel. I think we should be consistent across platforms

Copy link
Member Author

Choose a reason for hiding this comment

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

Great point, well spotted. I've changed it to retain the existing semantics.

// For now, this minimal work queue is an internal detail of how the framework dispatches
// incoming JS->.NET calls and makes sure they get deferred if a renderbatch is in process.

internal static class WebAssemblyCallQueue
Copy link
Member

Choose a reason for hiding this comment

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

This looks very similar to the RendererSynchronizationContext is there a way we could "reuse" that here instead of having a separate work queue?

These things are tricky to test implement and can lead to subtle bugs that are hard to track down

Copy link
Member Author

Choose a reason for hiding this comment

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

The main issue with using a sync context is the back-compat about enforcement of being "on" the sync context (discussed above), but even besides that, the existence of synchronous interop makes WebAssembly a different kind of runtime environment for which we can't assume that RendererSynchronizationContext has the right behaviors. For example, incoming calls may already be on the sync context but we still need to defer their work. It's not the right fit for this runtime.

I know your concern is more future-thinking and looks towards a scenario where we have real multithreading. My expectation is that when/if we get to that, we'll be in a position to recreate the same threading semantics as Server pretty much exactly (assuming we want to), but that's not the case today and shouldn't be considered in scope for fixing this bug. My goal in this PR is to wipe out the largest class of possible inconsistency issues I can while not regressing compat or perf.

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 great!

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 feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Exception thrown if FocusAsync() and @onfocusin are used in custom input element at the same time - blazor Wasm
6 participants