Skip to content

Commit 9800678

Browse files
committed
rebase
1 parent f31dd94 commit 9800678

28 files changed

+778
-29
lines changed

AspNetCore.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Server", "sr
13201320
EndProject
13211321
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StandaloneApp", "src\Components\WebAssembly\testassets\StandaloneApp\StandaloneApp.csproj", "{A40350FE-4334-4007-B1C3-6BEB1B070309}"
13221322
EndProject
1323+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThreadingApp", "src\Components\WebAssembly\testassets\ThreadingApp\ThreadingApp.csproj", "{A40350FE-4334-4007-B1C3-6BEB1B070308}"
1324+
EndProject
13231325
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HealthChecks", "HealthChecks", "{C1E7F837-6988-43E2-9E1C-7302DB484F99}"
13241326
EndProject
13251327
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}"
@@ -8232,6 +8234,22 @@ Global
82328234
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x64.Build.0 = Release|Any CPU
82338235
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x86.ActiveCfg = Release|Any CPU
82348236
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x86.Build.0 = Release|Any CPU
8237+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
8238+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|Any CPU.Build.0 = Debug|Any CPU
8239+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|arm64.ActiveCfg = Debug|Any CPU
8240+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|arm64.Build.0 = Debug|Any CPU
8241+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x64.ActiveCfg = Debug|Any CPU
8242+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x64.Build.0 = Debug|Any CPU
8243+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x86.ActiveCfg = Debug|Any CPU
8244+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x86.Build.0 = Debug|Any CPU
8245+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|Any CPU.ActiveCfg = Release|Any CPU
8246+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|Any CPU.Build.0 = Release|Any CPU
8247+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|arm64.ActiveCfg = Release|Any CPU
8248+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|arm64.Build.0 = Release|Any CPU
8249+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x64.ActiveCfg = Release|Any CPU
8250+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x64.Build.0 = Release|Any CPU
8251+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x86.ActiveCfg = Release|Any CPU
8252+
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x86.Build.0 = Release|Any CPU
82358253
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
82368254
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|Any CPU.Build.0 = Debug|Any CPU
82378255
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|arm64.ActiveCfg = Debug|Any CPU

src/Components/Components.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
3636
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
3737
"src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
38+
"src\\Components\\WebAssembly\\testassets\\ThreadingApp\\ThreadingApp.csproj",
3839
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Client\\Wasm.Prerendered.Client.csproj",
3940
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Server\\Wasm.Prerendered.Server.csproj",
4041
"src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj",

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree;
2424
// dispatching events to them, and notifying when the user interface is being updated.
2525
public abstract partial class Renderer : IDisposable, IAsyncDisposable
2626
{
27+
private readonly object _lockObject = new();
2728
private readonly IServiceProvider _serviceProvider;
2829
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
2930
private readonly Dictionary<IComponent, ComponentState> _componentStateByComponent = new Dictionary<IComponent, ComponentState>();
@@ -1102,18 +1103,33 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
11021103
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
11031104
protected virtual void Dispose(bool disposing)
11041105
{
1106+
lock (_lockObject)
1107+
{
1108+
if (_rendererIsDisposed)
1109+
{
1110+
return;
1111+
}
1112+
1113+
_rendererIsDisposed = true;
1114+
}
1115+
11051116
if (!Dispatcher.CheckAccess())
11061117
{
11071118
// It's important that we only call the components' Dispose/DisposeAsync lifecycle methods
11081119
// on the sync context, like other lifecycle methods. In almost all cases we'd already be
11091120
// on the sync context here since DisposeAsync dispatches, but just in case someone is using
11101121
// Dispose directly, we'll dispatch and block.
1111-
Dispatcher.InvokeAsync(() => Dispose(disposing)).Wait();
1122+
var done = Dispatcher.InvokeAsync(() => Dispose(disposing));
1123+
1124+
// only block caller when this is not finalizer
1125+
if (disposing)
1126+
{
1127+
done.Wait();
1128+
}
1129+
11121130
return;
11131131
}
11141132

1115-
_rendererIsDisposed = true;
1116-
11171133
if (_hotReloadInitialized && HotReloadManager.MetadataUpdateSupported)
11181134
{
11191135
HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
@@ -1195,7 +1211,7 @@ void NotifyExceptions(List<Exception> exceptions)
11951211
/// <summary>
11961212
/// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
11971213
/// This is only called when a render mode is specified either at the call site or on the component type.
1198-
///
1214+
///
11991215
/// Subclasses may override this method to return a component of a different type, or throw, depending on whether the renderer
12001216
/// supports the render mode and how it implements that support.
12011217
/// </summary>
@@ -1225,9 +1241,12 @@ public void Dispose()
12251241
/// <inheritdoc />
12261242
public async ValueTask DisposeAsync()
12271243
{
1228-
if (_rendererIsDisposed)
1244+
lock (_lockObject)
12291245
{
1230-
return;
1246+
if (_rendererIsDisposed)
1247+
{
1248+
return;
1249+
}
12311250
}
12321251

12331252
if (_disposeTask != null)

src/Components/Components/src/Rendering/RendererSynchronizationContextDispatcher.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public RendererSynchronizationContextDispatcher()
2020

2121
public override Task InvokeAsync(Action workItem)
2222
{
23+
ArgumentNullException.ThrowIfNull(workItem);
2324
if (CheckAccess())
2425
{
2526
workItem();
@@ -31,6 +32,7 @@ public override Task InvokeAsync(Action workItem)
3132

3233
public override Task InvokeAsync(Func<Task> workItem)
3334
{
35+
ArgumentNullException.ThrowIfNull(workItem);
3436
if (CheckAccess())
3537
{
3638
return workItem();
@@ -41,6 +43,7 @@ public override Task InvokeAsync(Func<Task> workItem)
4143

4244
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
4345
{
46+
ArgumentNullException.ThrowIfNull(workItem);
4447
if (CheckAccess())
4548
{
4649
return Task.FromResult(workItem());
@@ -51,6 +54,7 @@ public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
5154

5255
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
5356
{
57+
ArgumentNullException.ThrowIfNull(workItem);
5458
if (CheckAccess())
5559
{
5660
return workItem();

src/Components/ComponentsNoDeps.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
3535
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
3636
"src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
37+
"src\\Components\\WebAssembly\\testassets\\ThreadingApp\\ThreadingApp.csproj",
3738
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Client\\Wasm.Prerendered.Client.csproj",
3839
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Server\\Wasm.Prerendered.Server.csproj",
3940
"src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj",

src/Components/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ Please see the [`Build From Source`](https://github.com/dotnet/aspnetcore/blob/m
110110

111111
##### WebAssembly Trimming
112112

113-
By default, WebAssembly E2E tests that run as part of the CI or when run in Release builds run with trimming enabled. It's possible that tests that successfully run locally might fail as part of the CI run due to errors introduced due to trimming. To test this scenario locally, either run the E2E tests in release build or with the `TestTrimmedApps` property set. For e.g.
113+
By default, WebAssembly E2E tests that run as part of the CI or when run in Release builds run with trimming enabled. It's possible that tests that successfully run locally might fail as part of the CI run due to errors introduced due to trimming. To test this scenario locally, either run the E2E tests in release build or with the `TestTrimmedOrMultithreadingApps` property set. For e.g.
114114

115115
```
116116
dotnet test -c Release
117117
```
118118
or
119119
```
120-
dotnet build /p:TestTrimmedApps=true
120+
dotnet build /p:TestTrimmedOrMultithreadingApps=true
121121
dotnet test --no-build
122122
```
123123

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Components.Routing;
1111
using Microsoft.AspNetCore.Components.Web;
1212
using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
13+
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
1314
using Microsoft.AspNetCore.Components.WebAssembly.Services;
1415
using Microsoft.Extensions.Configuration;
1516
using Microsoft.Extensions.Configuration.Json;
@@ -75,6 +76,8 @@ internal WebAssemblyHostBuilder(
7576
Services = new ServiceCollection();
7677
Logging = new LoggingBuilder(Services);
7778

79+
InitializeWebAssemblyRenderer();
80+
7881
// Retrieve required attributes from JSRuntimeInvoker
7982
InitializeNavigationManager(jsMethods);
8083
InitializeRegisteredRootComponents(jsMethods);
@@ -176,6 +179,14 @@ private WebAssemblyHostEnvironment InitializeEnvironment(IInternalJSImportMethod
176179
return hostEnvironment;
177180
}
178181

182+
private static void InitializeWebAssemblyRenderer()
183+
{
184+
// capture the JSSynchronizationContext from the main thread, which runtime already installed.
185+
// if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
186+
// if user somehow installed SynchronizationContext different from JSSynchronizationContext, they need to make sure the behavior is consistent with JSSynchronizationContext.
187+
WebAssemblyRenderer._mainSynchronizationContext = SynchronizationContext.Current;
188+
}
189+
179190
/// <summary>
180191
/// Gets an <see cref="WebAssemblyHostConfiguration"/> that can be used to customize the application's
181192
/// configuration sources and read configuration attributes.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;
5+
6+
// When Blazor is deployed with multi-threaded runtime, WebAssemblyDispatcher will help to dispatch all Blazor JS interop calls to the main thread.
7+
// This is necessary because all JS objects have thread affinity. They are only available on the thread (WebWorker) which created them.
8+
// Also DOM is only available on the main (browser) thread.
9+
// Because all of the Dispatcher.InvokeAsync methods return Task, we don't need to propagate errors via OnUnhandledException handler
10+
internal sealed class WebAssemblyDispatcher : Dispatcher
11+
{
12+
private readonly SynchronizationContext _mainSyncContext;
13+
14+
public WebAssemblyDispatcher(SynchronizationContext mainSyncContext)
15+
{
16+
_mainSyncContext = mainSyncContext;
17+
}
18+
19+
public override bool CheckAccess() => SynchronizationContext.Current == _mainSyncContext;
20+
21+
public override Task InvokeAsync(Action workItem)
22+
{
23+
ArgumentNullException.ThrowIfNull(workItem);
24+
if (CheckAccess())
25+
{
26+
// this branch executes on correct thread and solved JavaScript objects thread affinity
27+
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
28+
workItem();
29+
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
30+
return Task.CompletedTask;
31+
}
32+
33+
var tcs = new TaskCompletionSource();
34+
35+
// RendererSynchronizationContext doesn't need to deal with thread affinity and so it could execute jobs on calling thread as optimization.
36+
// we could not do it for WASM/JavaScript, because we need to solve for thread affinity of JavaScript objects, so we always Post into the queue.
37+
_mainSyncContext.Post(static (object? o) =>
38+
{
39+
var state = ((TaskCompletionSource tcs, Action workItem))o!;
40+
try
41+
{
42+
state.workItem();
43+
state.tcs.SetResult();
44+
}
45+
catch (Exception ex)
46+
{
47+
state.tcs.SetException(ex);
48+
}
49+
}, (tcs, workItem));
50+
51+
return tcs.Task;
52+
}
53+
54+
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
55+
{
56+
ArgumentNullException.ThrowIfNull(workItem);
57+
if (CheckAccess())
58+
{
59+
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
60+
return Task.FromResult(workItem());
61+
}
62+
63+
var tcs = new TaskCompletionSource<TResult>();
64+
65+
_mainSyncContext.Post(static (object? o) =>
66+
{
67+
var state = ((TaskCompletionSource<TResult> tcs, Func<TResult> workItem))o!;
68+
try
69+
{
70+
var res = state.workItem();
71+
state.tcs.SetResult(res);
72+
}
73+
catch (Exception ex)
74+
{
75+
state.tcs.SetException(ex);
76+
}
77+
}, (tcs, workItem));
78+
79+
return tcs.Task;
80+
}
81+
82+
public override Task InvokeAsync(Func<Task> workItem)
83+
{
84+
ArgumentNullException.ThrowIfNull(workItem);
85+
if (CheckAccess())
86+
{
87+
// this branch executes on correct thread and solved JavaScript objects thread affinity
88+
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
89+
return workItem();
90+
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
91+
}
92+
93+
var tcs = new TaskCompletionSource();
94+
95+
_mainSyncContext.Post(static (object? o) =>
96+
{
97+
var state = ((TaskCompletionSource tcs, Func<Task> workItem))o!;
98+
99+
try
100+
{
101+
state.workItem().ContinueWith(t =>
102+
{
103+
if (t.IsFaulted)
104+
{
105+
state.tcs.SetException(t.Exception);
106+
}
107+
else
108+
{
109+
state.tcs.SetResult();
110+
}
111+
}, TaskScheduler.FromCurrentSynchronizationContext());
112+
}
113+
catch (Exception ex)
114+
{
115+
// it could happen that the workItem will throw synchronously
116+
state.tcs.SetException(ex);
117+
}
118+
}, (tcs, workItem));
119+
120+
return tcs.Task;
121+
}
122+
123+
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
124+
{
125+
ArgumentNullException.ThrowIfNull(workItem);
126+
if (CheckAccess())
127+
{
128+
// this branch executes on correct thread and solved JavaScript objects thread affinity
129+
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
130+
return workItem();
131+
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
132+
}
133+
134+
var tcs = new TaskCompletionSource<TResult>();
135+
136+
_mainSyncContext.Post(static (object? o) =>
137+
{
138+
var state = ((TaskCompletionSource<TResult> tcs, Func<Task<TResult>> workItem))o!;
139+
try
140+
{
141+
state.workItem().ContinueWith(t =>
142+
{
143+
if (t.IsFaulted)
144+
{
145+
state.tcs.SetException(t.Exception);
146+
}
147+
else
148+
{
149+
state.tcs.SetResult(t.Result);
150+
}
151+
}, TaskScheduler.FromCurrentSynchronizationContext());
152+
}
153+
catch (Exception ex)
154+
{
155+
state.tcs.SetException(ex);
156+
}
157+
}, (tcs, workItem));
158+
159+
return tcs.Task;
160+
}
161+
}

src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;
2222
internal sealed partial class WebAssemblyRenderer : WebRenderer
2323
{
2424
private readonly ILogger _logger;
25+
private readonly Dispatcher _dispatcher;
26+
internal static SynchronizationContext? _mainSynchronizationContext;
2527

2628
public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop)
2729
: base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop)
2830
{
2931
_logger = loggerFactory.CreateLogger<WebAssemblyRenderer>();
3032

33+
// if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
34+
_dispatcher = _mainSynchronizationContext == null
35+
? NullDispatcher.Instance
36+
: new WebAssemblyDispatcher(_mainSynchronizationContext);
37+
3138
ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext;
3239
DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents;
3340
}
@@ -69,7 +76,7 @@ public static void NotifyEndUpdateRootComponents(long batchId)
6976
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId);
7077
}
7178

72-
public override Dispatcher Dispatcher => NullDispatcher.Instance;
79+
public override Dispatcher Dispatcher => _dispatcher;
7380

7481
public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type componentType, ParameterView parameters, string domElementSelector)
7582
{

0 commit comments

Comments
 (0)