Skip to content

Commit

Permalink
rebase
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelsavara committed Jan 3, 2024
1 parent f31dd94 commit 9800678
Show file tree
Hide file tree
Showing 28 changed files with 778 additions and 29 deletions.
18 changes: 18 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Server", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StandaloneApp", "src\Components\WebAssembly\testassets\StandaloneApp\StandaloneApp.csproj", "{A40350FE-4334-4007-B1C3-6BEB1B070309}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThreadingApp", "src\Components\WebAssembly\testassets\ThreadingApp\ThreadingApp.csproj", "{A40350FE-4334-4007-B1C3-6BEB1B070308}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HealthChecks", "HealthChecks", "{C1E7F837-6988-43E2-9E1C-7302DB484F99}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}"
Expand Down Expand Up @@ -8232,6 +8234,22 @@ Global
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x64.Build.0 = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x86.ActiveCfg = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070309}.Release|x86.Build.0 = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|arm64.ActiveCfg = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|arm64.Build.0 = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x64.ActiveCfg = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x64.Build.0 = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x86.ActiveCfg = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Debug|x86.Build.0 = Debug|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|Any CPU.Build.0 = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|arm64.ActiveCfg = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|arm64.Build.0 = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x64.ActiveCfg = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x64.Build.0 = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x86.ActiveCfg = Release|Any CPU
{A40350FE-4334-4007-B1C3-6BEB1B070308}.Release|x86.Build.0 = Release|Any CPU
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B06040BC-DA28-4923-8CAC-20EB517D471B}.Debug|arm64.ActiveCfg = Debug|Any CPU
Expand Down
1 change: 1 addition & 0 deletions src/Components/Components.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
"src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
"src\\Components\\WebAssembly\\testassets\\ThreadingApp\\ThreadingApp.csproj",
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Client\\Wasm.Prerendered.Client.csproj",
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Server\\Wasm.Prerendered.Server.csproj",
"src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj",
Expand Down
31 changes: 25 additions & 6 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree;
// dispatching events to them, and notifying when the user interface is being updated.
public abstract partial class Renderer : IDisposable, IAsyncDisposable
{
private readonly object _lockObject = new();
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
private readonly Dictionary<IComponent, ComponentState> _componentStateByComponent = new Dictionary<IComponent, ComponentState>();
Expand Down Expand Up @@ -1102,18 +1103,33 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
protected virtual void Dispose(bool disposing)
{
lock (_lockObject)
{
if (_rendererIsDisposed)
{
return;
}

_rendererIsDisposed = true;
}

if (!Dispatcher.CheckAccess())
{
// It's important that we only call the components' Dispose/DisposeAsync lifecycle methods
// on the sync context, like other lifecycle methods. In almost all cases we'd already be
// on the sync context here since DisposeAsync dispatches, but just in case someone is using
// Dispose directly, we'll dispatch and block.
Dispatcher.InvokeAsync(() => Dispose(disposing)).Wait();
var done = Dispatcher.InvokeAsync(() => Dispose(disposing));

// only block caller when this is not finalizer
if (disposing)
{
done.Wait();
}

return;
}

_rendererIsDisposed = true;

if (_hotReloadInitialized && HotReloadManager.MetadataUpdateSupported)
{
HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
Expand Down Expand Up @@ -1195,7 +1211,7 @@ void NotifyExceptions(List<Exception> exceptions)
/// <summary>
/// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
/// This is only called when a render mode is specified either at the call site or on the component type.
///
///
/// Subclasses may override this method to return a component of a different type, or throw, depending on whether the renderer
/// supports the render mode and how it implements that support.
/// </summary>
Expand Down Expand Up @@ -1225,9 +1241,12 @@ public void Dispose()
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (_rendererIsDisposed)
lock (_lockObject)
{
return;
if (_rendererIsDisposed)
{
return;
}
}

if (_disposeTask != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public RendererSynchronizationContextDispatcher()

public override Task InvokeAsync(Action workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
workItem();
Expand All @@ -31,6 +32,7 @@ public override Task InvokeAsync(Action workItem)

public override Task InvokeAsync(Func<Task> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
return workItem();
Expand All @@ -41,6 +43,7 @@ public override Task InvokeAsync(Func<Task> workItem)

public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
return Task.FromResult(workItem());
Expand All @@ -51,6 +54,7 @@ public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)

public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
return workItem();
Expand Down
1 change: 1 addition & 0 deletions src/Components/ComponentsNoDeps.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
"src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
"src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
"src\\Components\\WebAssembly\\testassets\\ThreadingApp\\ThreadingApp.csproj",
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Client\\Wasm.Prerendered.Client.csproj",
"src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Server\\Wasm.Prerendered.Server.csproj",
"src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj",
Expand Down
4 changes: 2 additions & 2 deletions src/Components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ Please see the [`Build From Source`](https://github.com/dotnet/aspnetcore/blob/m

##### WebAssembly Trimming

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.
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.

```
dotnet test -c Release
```
or
```
dotnet build /p:TestTrimmedApps=true
dotnet build /p:TestTrimmedOrMultithreadingApps=true
dotnet test --no-build
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
Expand Down Expand Up @@ -75,6 +76,8 @@ internal WebAssemblyHostBuilder(
Services = new ServiceCollection();
Logging = new LoggingBuilder(Services);

InitializeWebAssemblyRenderer();

// Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsMethods);
InitializeRegisteredRootComponents(jsMethods);
Expand Down Expand Up @@ -176,6 +179,14 @@ private WebAssemblyHostEnvironment InitializeEnvironment(IInternalJSImportMethod
return hostEnvironment;
}

private static void InitializeWebAssemblyRenderer()
{
// capture the JSSynchronizationContext from the main thread, which runtime already installed.
// if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
// if user somehow installed SynchronizationContext different from JSSynchronizationContext, they need to make sure the behavior is consistent with JSSynchronizationContext.
WebAssemblyRenderer._mainSynchronizationContext = SynchronizationContext.Current;
}

/// <summary>
/// Gets an <see cref="WebAssemblyHostConfiguration"/> that can be used to customize the application's
/// configuration sources and read configuration attributes.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;

// When Blazor is deployed with multi-threaded runtime, WebAssemblyDispatcher will help to dispatch all Blazor JS interop calls to the main thread.
// This is necessary because all JS objects have thread affinity. They are only available on the thread (WebWorker) which created them.
// Also DOM is only available on the main (browser) thread.
// Because all of the Dispatcher.InvokeAsync methods return Task, we don't need to propagate errors via OnUnhandledException handler
internal sealed class WebAssemblyDispatcher : Dispatcher
{
private readonly SynchronizationContext _mainSyncContext;

public WebAssemblyDispatcher(SynchronizationContext mainSyncContext)
{
_mainSyncContext = mainSyncContext;
}

public override bool CheckAccess() => SynchronizationContext.Current == _mainSyncContext;

public override Task InvokeAsync(Action workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
// this branch executes on correct thread and solved JavaScript objects thread affinity
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
workItem();
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
return Task.CompletedTask;
}

var tcs = new TaskCompletionSource();

// RendererSynchronizationContext doesn't need to deal with thread affinity and so it could execute jobs on calling thread as optimization.
// 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.
_mainSyncContext.Post(static (object? o) =>
{
var state = ((TaskCompletionSource tcs, Action workItem))o!;
try
{
state.workItem();
state.tcs.SetResult();
}
catch (Exception ex)
{
state.tcs.SetException(ex);
}
}, (tcs, workItem));

return tcs.Task;
}

public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
return Task.FromResult(workItem());
}

var tcs = new TaskCompletionSource<TResult>();

_mainSyncContext.Post(static (object? o) =>
{
var state = ((TaskCompletionSource<TResult> tcs, Func<TResult> workItem))o!;
try
{
var res = state.workItem();
state.tcs.SetResult(res);
}
catch (Exception ex)
{
state.tcs.SetException(ex);
}
}, (tcs, workItem));

return tcs.Task;
}

public override Task InvokeAsync(Func<Task> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
// this branch executes on correct thread and solved JavaScript objects thread affinity
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
return workItem();
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
}

var tcs = new TaskCompletionSource();

_mainSyncContext.Post(static (object? o) =>
{
var state = ((TaskCompletionSource tcs, Func<Task> workItem))o!;

try
{
state.workItem().ContinueWith(t =>
{
if (t.IsFaulted)
{
state.tcs.SetException(t.Exception);
}
else
{
state.tcs.SetResult();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
// it could happen that the workItem will throw synchronously
state.tcs.SetException(ex);
}
}, (tcs, workItem));

return tcs.Task;
}

public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
if (CheckAccess())
{
// this branch executes on correct thread and solved JavaScript objects thread affinity
// but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
return workItem();
// it can throw synchronously, same as RendererSynchronizationContextDispatcher
}

var tcs = new TaskCompletionSource<TResult>();

_mainSyncContext.Post(static (object? o) =>
{
var state = ((TaskCompletionSource<TResult> tcs, Func<Task<TResult>> workItem))o!;
try
{
state.workItem().ContinueWith(t =>
{
if (t.IsFaulted)
{
state.tcs.SetException(t.Exception);
}
else
{
state.tcs.SetResult(t.Result);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
state.tcs.SetException(ex);
}
}, (tcs, workItem));

return tcs.Task;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;
internal sealed partial class WebAssemblyRenderer : WebRenderer
{
private readonly ILogger _logger;
private readonly Dispatcher _dispatcher;
internal static SynchronizationContext? _mainSynchronizationContext;

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

// if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
_dispatcher = _mainSynchronizationContext == null
? NullDispatcher.Instance
: new WebAssemblyDispatcher(_mainSynchronizationContext);

ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext;
DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents;
}
Expand Down Expand Up @@ -69,7 +76,7 @@ public static void NotifyEndUpdateRootComponents(long batchId)
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId);
}

public override Dispatcher Dispatcher => NullDispatcher.Instance;
public override Dispatcher Dispatcher => _dispatcher;

public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type componentType, ParameterView parameters, string domElementSelector)
{
Expand Down
Loading

0 comments on commit 9800678

Please sign in to comment.