Skip to content
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

Merged
merged 9 commits into from
Jul 1, 2024

Conversation

eerhardt
Copy link
Member

@eerhardt eerhardt commented Jun 25, 2024

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

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.
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-signalr Includes: SignalR clients and servers label Jun 25, 2024
@eerhardt eerhardt mentioned this pull request Jun 25, 2024
1 task
}

private static IEnumerable<MethodInfo> GetAllInterfaceMethods(Type interfaceType)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern",
Copy link
Member Author

Choose a reason for hiding this comment

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

* 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>.
@eerhardt eerhardt marked this pull request as ready for review June 26, 2024 22:18
@@ -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")]
Copy link
Member

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

Copy link
Member Author

@eerhardt eerhardt Jun 27, 2024

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:

Copy link
Member Author

@eerhardt eerhardt Jun 27, 2024

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();
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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

Assert.Single(TestSink.Writes.Where(write => write.EventId.Name == "ErrorDispatchingHubEvent"));

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 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>
Copy link
Member

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?

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 would, but RequiresDynamicCode can only go on methods, constructors, and classes.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
internal sealed class RequiresDynamicCodeAttribute : Attribute

Copy link
Member

@BrennanConroy BrennanConroy left a 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">
Copy link
Member

Choose a reason for hiding this comment

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

What is a .proj?

Copy link
Member Author

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.

<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}"
Copy link
Member

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?

Copy link
Member Author

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",
Copy link
Member

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.
Copy link
Member

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?

Copy link
Member Author

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.

image

You can learn more about this at https://github.com/dotnet/aspnetcore/blob/main/docs/Trimming.md#validate-trimming-behavior.

@eerhardt
Copy link
Member Author

eerhardt commented Jul 1, 2024

Do you have a size comparison?

Taking a dotnet new webapiaot project and publishing it for win-x64 on my machine, the resulting .exe is 8.73 MB (9,163,776 bytes).

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 10.9 MB (11,445,760 bytes).

So roughly 2.2 MB of additional size to add SignalR server. Doing a really quick sizoscope on the before and after, here are my thoughts on what could be eliminated if we wanted:

  1. System.Text.RegularExpressions is getting pulled in, which is adding a lot of the size we cut out of the "Slim" builder. The reason it gets pulled in is because this calls AddRouting and not AddRoutingCore.

    public static IServiceCollection AddConnections(this IServiceCollection services)
    {
    services.AddRouting();

  2. Super minor would be to remove the Linq usage in

    public static IEnumerable<MethodInfo> GetHubMethods(Type hubType)

    and
    ParameterTypes = methodExecutor.MethodParameters.Where((p, index) =>

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.

@eerhardt eerhardt merged commit c02ff12 into dotnet:main Jul 1, 2024
26 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the 9.0-preview7 milestone Jul 1, 2024
@eerhardt eerhardt deleted the SignalRNativeAOT-Server branch July 1, 2024 22:04
@BrennanConroy
Copy link
Member

I was more asking how much this PR saves vs. not trimming SignalR.

@eerhardt
Copy link
Member Author

eerhardt commented Jul 1, 2024

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

eerhardt added a commit to dotnet/sdk that referenced this pull request Jul 9, 2024
This is the SDK portion of changes needed to make SignalR compatible with native AOT. See dotnet/aspnetcore#56460.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr Includes: SignalR clients and servers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants