-
Notifications
You must be signed in to change notification settings - Fork 10k
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
Annotate SignalR server for native AOT #56460
Conversation
Fix SignalR Server's usage of MakeGenericMethod when using a streaming reader (IAsyncEnumerable or ChannelReader) following the same approach as the client. Add a runtime check and throw an exception when trying to stream a ValueType in native AOT. Adjust the public annotations: * Remove RequiresUnreferencedCode from AddSignalR * Add RequiresUnreferencedCode to MessagePack and NewtonsoftJson protocols Support trimming and AOT in DefaultHubDispatcher by adding a feature switch to turn off custom awaitable support. Update ObjectMethodExecutor to support trimming and AOT by a new method that doesn't look for custom awaitables, and uses reflection instead of Linq.Expressions.
} | ||
|
||
private static IEnumerable<MethodInfo> GetAllInterfaceMethods(Type interfaceType) | ||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", |
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.
Note the suppressions in this file are the same as in https://github.com/dotnet/runtime/blob/3ec76324514868b0ee4cfcca43ef96a4d62d44fa/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs#L168-L174
* Move RequiresDynamicCode to the whole Hub<T> class, so developers always get a warning when using IHubContext<THub, T>. This helps because THub needs to be a Hub<T>, which will warn as soon as it is used. * Suppress the warning in AddSignalRCore that references HubContext<THub, T> since users will get warnings when they try using IHubContext<THub, T>.
@@ -31,6 +32,12 @@ internal sealed partial class DefaultHubDispatcher<THub> : HubDispatcher<THub> w | |||
private readonly Func<HubLifetimeContext, Exception?, Task>? _onDisconnectedMiddleware; | |||
private readonly HubLifetimeManager<THub> _hubLifetimeManager; | |||
|
|||
[FeatureSwitchDefinition("Microsoft.AspNetCore.SignalR.Hub.CustomAwaitableSupport")] |
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.
Could probably remove Hub
from the name:
Microsoft.AspNetCore.SignalR.CustomAwaitableSupport
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.
My recommendation on feature switches is to name them in such a way in case we ever need to make them as a public API, like we did with JsonSerializer.IsReflectionEnabledByDefault. So that's the pattern I've been following lately.
See also:
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.
FYI - I'm renaming this switch to Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported
to align with the naming of other feature switches:
- System.Diagnostics.Tracing.EventSource.IsSupported
- System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported
- System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault
- System.ComponentModel.TypeDescriptor.IsComObjectDescriptorSupported
I just don't want to reset CI right now, so will push that change after CI is done running.
This is now done.
RunNativeAotTest(static async () => | ||
{ | ||
//System.Diagnostics.Debugger.Launch(); | ||
var loggerFactory = new StringLoggerFactory(); |
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.
Can we remove this? If you call StartServer
it already configures a logger factory that is wired up to file logging (and VS logging).
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.
StartServer
requires you to pass an ILoggerFactory in:
public static async Task<InProcessTestServer<TStartup>> StartServer(ILoggerFactory loggerFactory, Action<KestrelServerOptions> configureKestrelServerOptions = null, IDisposable disposable = null) |
Also, we need to assert the logs contains the right string for the tests that return just Task/ValueTask to ensure they got called correctly.
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.
public Task<InProcessTestServer<T>> StartServer<T>(Func<WriteContext, bool> expectedErrorsFilter = null, Action<KestrelServerOptions> configureKestrelServerOptions = null) where T : class |
aspnetcore/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs
Line 96 in 250b489
Assert.Single(TestSink.Writes.Where(write => write.EventId.Name == "ErrorDispatchingHubEvent")); |
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.
The problem with that is that this code is running in a new process (spun up by the RemoteExecutor) and so we don't have access to the instance of the Test. And things like TestSink
and ITestOutputHelper
aren't available in the separate process.
namespace Microsoft.AspNetCore.SignalR; | ||
|
||
/// <summary> | ||
/// A context abstraction for a hub. | ||
/// </summary> | ||
public interface IHubContext<THub, T> | ||
public interface IHubContext<THub, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> |
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.
Did we want to put RequiresDynamicCode on this type like we did for the internal implementation?
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.
I would, but RequiresDynamicCode can only go on methods, constructors, and classes.
aspnetcore/src/Shared/TrimmingAttributes.cs
Lines 16 to 17 in 0da8ea7
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] | |
internal sealed class RequiresDynamicCodeAttribute : Attribute |
… other feature switch naming
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.
Do you have a size comparison?
@@ -0,0 +1,9 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> |
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.
What is a .proj?
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.
It is an MSBuild project that doesn't have a language like a .csproj
or a .vbproj
does. It isn't technically part of MSBuild, but just a convention for a project that executes arbitrary MSBuild targets. (in this case, it generates a new .csproj
from a template, publishes it, executes the pubilshed app, and verifies the app returned exit code 100
.
Another example is the helixpublish.proj
that publishes tests to helix.
aspnetcore/eng/common/helixpublish.proj
Lines 15 to 19 in 8046091
<HelixWorkItem Include="WorkItem" Condition="'$(WorkItemDirectory)' != ''"> | |
<PayloadDirectory>$(WorkItemDirectory)</PayloadDirectory> | |
<Command>$(WorkItemCommand)</Command> | |
<Timeout Condition="'$(WorkItemTimeout)' != ''">$(WorkItemTimeout)</Timeout> | |
</HelixWorkItem> |
@@ -1102,7 +1102,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{5095E70C-6 | |||
EndProject | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Microbenchmarks", "src\SignalR\perf\Microbenchmarks\Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj", "{A6A95BEF-7E21-4D3D-921B-F77267219D27}" | |||
EndProject | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests", "src\SignalR\server\SignalR\test\Microsoft.AspNetCore.SignalR.Tests.csproj", "{4DC9C494-9867-4319-937E-5FBC0E5F5A51}" | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests", "src\SignalR\server\SignalR\test\Microsoft.AspNetCore.SignalR.Tests\Microsoft.AspNetCore.SignalR.Tests.csproj", "{4DC9C494-9867-4319-937E-5FBC0E5F5A51}" |
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.
Add src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj?
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.
We don't put these in the .sln because they aren't "normal" projects, so you don't get intellisense editing or anything like that. Because they are self-contained executables, the UX for working on these tests is different than our other tests.
@@ -68,7 +68,7 @@ | |||
"src\\SignalR\\samples\\WebSocketSample\\WebSocketSample.csproj", | |||
"src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj", | |||
"src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj", | |||
"src\\SignalR\\server\\SignalR\\test\\Microsoft.AspNetCore.SignalR.Tests.csproj", | |||
"src\\SignalR\\server\\SignalR\\test\\Microsoft.AspNetCore.SignalR.Tests\\Microsoft.AspNetCore.SignalR.Tests.csproj", |
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.
Add src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj?
@@ -0,0 +1,84 @@ | |||
// Licensed to the .NET Foundation under one or more agreements. |
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.
What mechanism is running this app?
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.
We have test infrastructure that runs all these *.TrimmingTests.proj
and *.NativeAotTests.proj
projects.
They run on the build machines on the "Test" legs of CI.
You can learn more about this at https://github.com/dotnet/aspnetcore/blob/main/docs/Trimming.md#validate-trimming-behavior.
Taking a Then adding the following to the app: Program.cs var builder = WebApplication.CreateSlimBuilder(args);
...
+builder.Services.AddSignalR();
+builder.Services.Configure<JsonHubProtocolOptions>(o =>
+{
+ o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});
var app = builder.Build();
...
+app.MapHub<ChatHub>("/chathub");
app.Run(); ChatHub.cs using Microsoft.AspNetCore.SignalR;
public class ChatHub(ILogger<ChatHub> logger) : Hub
{
public async Task SendMessage(string user, string message)
{
logger.LogInformation("Received message from {user}: {message}", user, message);
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
} Publishing again, the resulting .exe is So roughly
I think we are in a good spot right now though. I opened an issue for the Regex change, since that would have the biggest benefit. |
I was more asking how much this PR saves vs. not trimming SignalR. |
If I take the same project as above and make the following changes to the .csproj: <!-- <PublishAot>true</PublishAot> -->
<PublishSingleFile>true</PublishSingleFile> The resulting .exe is |
This is the SDK portion of changes needed to make SignalR compatible with native AOT. See dotnet/aspnetcore#56460.
Fix SignalR Server's usage of MakeGenericMethod when using a streaming reader (IAsyncEnumerable or ChannelReader) following the same approach as the client. Add a runtime check and throw an exception when trying to stream a ValueType in native AOT.
Adjust the public annotations:
Support trimming and AOT in DefaultHubDispatcher by adding a feature switch to turn off custom awaitable support. Update ObjectMethodExecutor to support trimming and AOT by a new method that doesn't look for custom awaitables, and uses reflection instead of Linq.Expressions. This will need a corresponding PR in https://github.com/dotnet/sdk to default the feature switch value when PublishTrimmed or PublishAot is set to
true
.FYI - @agocke @MichalStrehovsky @sbomer - in case you want to take a look.