* Add failing test to show the issue * Make RemoteRenderer serialize render calls on sync context
This commit is contained in:
parent
e71db85149
commit
a48260a5c9
|
|
@ -40,8 +40,8 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
|
||||||
var scope = _scopeFactory.CreateScope();
|
var scope = _scopeFactory.CreateScope();
|
||||||
var jsRuntime = new RemoteJSRuntime(client);
|
var jsRuntime = new RemoteJSRuntime(client);
|
||||||
var rendererRegistry = new RendererRegistry();
|
var rendererRegistry = new RendererRegistry();
|
||||||
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client);
|
|
||||||
var synchronizationContext = new CircuitSynchronizationContext();
|
var synchronizationContext = new CircuitSynchronizationContext();
|
||||||
|
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
|
||||||
|
|
||||||
var circuitHost = new CircuitHost(
|
var circuitHost = new CircuitHost(
|
||||||
scope,
|
scope,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
private readonly IClientProxy _client;
|
private readonly IClientProxy _client;
|
||||||
private readonly IJSRuntime _jsRuntime;
|
private readonly IJSRuntime _jsRuntime;
|
||||||
private readonly RendererRegistry _rendererRegistry;
|
private readonly RendererRegistry _rendererRegistry;
|
||||||
|
private readonly SynchronizationContext _syncContext;
|
||||||
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
|
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
|
||||||
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
|
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
|
||||||
private long _nextRenderId = 1;
|
private long _nextRenderId = 1;
|
||||||
|
|
@ -40,16 +41,19 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
/// <param name="rendererRegistry">The <see cref="RendererRegistry"/>.</param>
|
/// <param name="rendererRegistry">The <see cref="RendererRegistry"/>.</param>
|
||||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
|
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
|
||||||
/// <param name="client">The <see cref="IClientProxy"/>.</param>
|
/// <param name="client">The <see cref="IClientProxy"/>.</param>
|
||||||
|
/// <param name="syncContext">A <see cref="SynchronizationContext"/> that can be used to serialize renderer operations.</param>
|
||||||
public RemoteRenderer(
|
public RemoteRenderer(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
RendererRegistry rendererRegistry,
|
RendererRegistry rendererRegistry,
|
||||||
IJSRuntime jsRuntime,
|
IJSRuntime jsRuntime,
|
||||||
IClientProxy client)
|
IClientProxy client,
|
||||||
|
SynchronizationContext syncContext)
|
||||||
: base(serviceProvider)
|
: base(serviceProvider)
|
||||||
{
|
{
|
||||||
_rendererRegistry = rendererRegistry;
|
_rendererRegistry = rendererRegistry;
|
||||||
_jsRuntime = jsRuntime;
|
_jsRuntime = jsRuntime;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_syncContext = syncContext;
|
||||||
|
|
||||||
_id = _rendererRegistry.Add(this);
|
_id = _rendererRegistry.Add(this);
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +99,19 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
_rendererRegistry.TryRemove(_id);
|
_rendererRegistry.TryRemove(_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||||
|
{
|
||||||
|
// Render operations are not thread-safe, so they need to be serialized.
|
||||||
|
// This also ensures that when the renderer invokes component lifecycle
|
||||||
|
// methods, it does so on the expected sync context.
|
||||||
|
// We have to use "Post" (for async execution) because if it blocked, it
|
||||||
|
// could deadlock when a child triggers a parent re-render.
|
||||||
|
_syncContext.Post(_ =>
|
||||||
|
{
|
||||||
|
base.AddToRenderQueue(componentId, renderFragment);
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
frame = frame.WithAttributeEventHandlerId(id);
|
frame = frame.WithAttributeEventHandlerId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
/// <summary>
|
||||||
|
/// Schedules a render for the specified <paramref name="componentId"/>. Its display
|
||||||
|
/// will be populated using the specified <paramref name="renderFragment"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentId">The ID of the component to render.</param>
|
||||||
|
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
|
||||||
|
protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||||
{
|
{
|
||||||
var componentState = GetOptionalComponentState(componentId);
|
var componentState = GetOptionalComponentState(componentId);
|
||||||
if (componentState == null)
|
if (componentState == null)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Configuration.Assemblies;
|
using System.Configuration.Assemblies;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BasicTestApp;
|
using BasicTestApp;
|
||||||
using BasicTestApp.HierarchicalImportsTest.Subdir;
|
using BasicTestApp.HierarchicalImportsTest.Subdir;
|
||||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
||||||
|
|
@ -534,6 +535,23 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
e => Assert.Equal("End", e.Text));
|
e => Assert.Equal("End", e.Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanAcceptSimultaneousRenderRequests()
|
||||||
|
{
|
||||||
|
var expectedOutput = string.Join(
|
||||||
|
string.Empty,
|
||||||
|
Enumerable.Range(0, 100).Select(_ => "😊"));
|
||||||
|
|
||||||
|
var appElement = MountTestComponent<ConcurrentRenderParent>();
|
||||||
|
|
||||||
|
// It's supposed to pause the rendering for this long. The WaitAssert below
|
||||||
|
// allows it to take up extra time if needed.
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
var outputElement = appElement.FindElement(By.Id("concurrent-render-output"));
|
||||||
|
WaitAssert.Equal(expectedOutput, () => outputElement.Text);
|
||||||
|
}
|
||||||
|
|
||||||
static IAlert SwitchToAlert(IWebDriver driver)
|
static IAlert SwitchToAlert(IWebDriver driver)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -1130,7 +1130,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
// Act/Assert 3: After we complete the first UI update in which a given
|
// Act/Assert 3: After we complete the first UI update in which a given
|
||||||
// event handler ID is disposed, we can no longer reuse that event handler ID
|
// event handler ID is disposed, we can no longer reuse that event handler ID
|
||||||
render1TCS.SetResult(null);
|
render1TCS.SetResult(null);
|
||||||
await Task.Delay(100); // From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough).
|
await Task.Delay(500); // From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough).
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
|
renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<span>@(isAfterDelay ? "😊" :"WAITING")</span>
|
||||||
|
@functions
|
||||||
|
{
|
||||||
|
protected bool isAfterDelay;
|
||||||
|
|
||||||
|
protected override async Task OnInitAsync()
|
||||||
|
{
|
||||||
|
// If there are lots of instances of this component, the following delay
|
||||||
|
// will result in a lot of them triggering a re-render simultaneously
|
||||||
|
// on different threads.
|
||||||
|
// This test is to verify that the renderer correctly accepts all the
|
||||||
|
// simultaneous render requests.
|
||||||
|
await Task.Delay(1000);
|
||||||
|
isAfterDelay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<p>
|
||||||
|
After a 1 second delay, the output should be 100x😊, with no remaining "WAITING" markers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="concurrent-render-output">
|
||||||
|
@for (var i = 0; i < 100; i++) {<ConcurrentRenderChild />}
|
||||||
|
</div>
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
|
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
|
||||||
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
||||||
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
|
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
|
||||||
|
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@if (SelectedComponentType != null)
|
@if (SelectedComponentType != null)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue