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

[AOT] Fix runtime warnings #45604

Merged
merged 45 commits into from
Jan 4, 2023
Merged

[AOT] Fix runtime warnings #45604

merged 45 commits into from
Jan 4, 2023

Conversation

JamesNK
Copy link
Member

@JamesNK JamesNK commented Dec 15, 2022

Addresses #45473

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

This seems like an improvement to me. I do wonder if we should have stronger runtime checks for the container and identity stuff. Maybe we should be adding new : class generic constraints to some APIs.

Should we file a tracking issue to remove the pragmas once dotnet/runtime#79425 is merged?

Comment on lines 345 to 347
if (containerType.IsValueType && !RuntimeFeature.IsDynamicCodeSupported)
{
throw new InvalidOperationException("ValueType startup container isn't supported with AOT.");
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is a valid approach to avoid the warning. This breaks the principle that "if you don't get any warnings, your app will work the same before and after publishing AOT".

If someone was using this functionality, and published for AOT, they wouldn't get any warnings and their app would work successfully at F5/dotnet run time. But when they went to publish the app, it would be broken.

Note this is different than the approach taken in dotnet/runtime#79425. The difference is that DI is using a "feature switch" which gets enabled when PublishAot=true is set in the .csproj. That approach means that the app will still fail at F5/dotnet run time, if the app was using the AOT-incompatible functionality.

I think the existing hosting APIs should just be marked RequiresDynamicCode instead of suppressing these warnings.

Copy link
Member

Choose a reason for hiding this comment

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

The same applies for the StartupLoader code/suppression.

Copy link
Member Author

@JamesNK JamesNK Dec 15, 2022

Choose a reason for hiding this comment

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

Startup containers are very rarely used. And I doubt anyone has created a struct startup container. I don't want to annotate the hosting APIs as RequiresDynamicCode for this one weird edge case.

Good point about dotnet run/dotnet publish difference. Should we add a flag like dotnet/runtime#79425 does that ASP.NET Core can use to validate AOT compatibility? Or should there be one flag that is shared by libraries/applications?

I think the use case might be shared between many libraries: AOT always works except in unusual and rare situations. The pragmatic approach is to have a runtime error instead of always warning with annotations.

Copy link
Member

Choose a reason for hiding this comment

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

@vitek-karas @MichalStrehovsky @jkotas - thoughts on converting the DI feature switch in dotnet/runtime#79425 to be a general purpose “Verify AOT Compatibility” feature switch that can be used by other libraries? Like for instance, here in aspnet?

Copy link
Member

Choose a reason for hiding this comment

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

Another option is to always throw an exception for value types, AOT or not, if we think this functionality isn’t used with value types.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's an option. We can publish it as a breaking change.

IMO regardless of whether we use it here or not, I think a generalized runtime verify AOT compatibility flag has some use. I think it provides a pragmatic middle ground in gray situations where something is compatible 99% of the time. Right now we have to choose between suppress and pretend everything is ok, and warn for everyone.

Copy link
Member

Choose a reason for hiding this comment

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

general purpose “Verify AOT Compatibility” feature switch

This sounds like dotnet/runtime#39806

Copy link
Member

Choose a reason for hiding this comment

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

Having something like dotnet/runtime#39806 would be nice.

You're then left with a choice:

  • Just disable the problematic case always (in our case here, always throw and potentially add class constrains in right places) - breaking change
  • Rely on Add ability to disable reflection emit for testing runtime#39806 to basically trigger IsDynamicCodeSupported based on a feature switch for you. Downside is that such solution is not granular - and so it would not be possible to disable dynamic code everywhere, except this one case.
  • Introduce a new feature switch - which is what Remove RequiresDynamicCode from Microsoft.Extensions.DependencyInjection runtime#79425 is doing. Similar to the above, but it adds granularity. Downside is the added complexity and "yet another feature switch"
  • Mark the API as AOT incompatible via the attributes - and hopefully figure out a new API which can be used instead

I think which one to do is a function of the specific scenario. If it's something super rare, I would go with either outright breaking change or generic feature switch. If it's still rare, but "real", then a dedicated feature switch might be better.

@JamesNK
Copy link
Member Author

JamesNK commented Dec 16, 2022

I've reviewed all the PR comments and changes in this PR. I've simplified some annotations and removed some unnecessary ones.

Some items are still in discussion (startup container, attributes on problem details converters) and work todo (fix problem details errors serialization in converter). Everything else is resolved/improved.

Comment on lines +77 to +93
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(decimal))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(Uri))]
[JsonSerializable(typeof(TimeSpan))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(DateTimeOffset))]
Copy link
Member

@davidfowl davidfowl Dec 21, 2022

Choose a reason for hiding this comment

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

We should undo this given our discussion @JamesNK

Copy link
Member Author

Choose a reason for hiding this comment

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

This is useful because it allows common type values as extension values to just work, e.g. adding a string value to problem details extensions.

Other than a small size cost when problem details aren't trimmed away, this doesn't hurt. Are you sure you want it removed?

Copy link
Member

Choose a reason for hiding this comment

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

I don't mind keeping it for now and removing it with @brunolins16's JSON changes, but I don't think this is generally useful.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok! I'd like to keep as is in this PR (rather than reverting to what it was, which doesn't work), and we can iterate on it during .NET 8 as we learn more.

@@ -18,6 +18,8 @@ public static partial class HttpResponseJsonExtensions
{
private const string RequiresUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. " +
Copy link
Member

Choose a reason for hiding this comment

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

@brunolins16 this will all be undone with your changes right?

Copy link
Member

Choose a reason for hiding this comment

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

I don't believe, the unsafe API will still need to be attributed, however, RDF + MVC should not call them anymore.

@JamesNK
Copy link
Member Author

JamesNK commented Jan 3, 2023

@eerhardt @davidfowl All feedback applied. Are there any other changes, or can this get approved and merged 🙏

@JamesNK JamesNK requested a review from a team as a code owner January 3, 2023 23:14
Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

This looks like a great step forward! Thanks for all the great work here.

I just had a few nits, a question, and a follow-up recommendation for the future.

This LGTM. Let's get this in and make more progress.

"Map methods that configure a RequestDelegate don't use trimmer unsafe features.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "We surface a RequiresDynamicCode in the call to the Map methods adding route handlers this EndpointDataSource. Analysis is unable to infer this. " +
"Map methods that configure a RequestDelegate don't use AOT unsafe features.")]
Copy link
Member

Choose a reason for hiding this comment

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

Is there some way we can verify this?

One idea is to add a "Native AOT"-test that uses filter factories and ensures they work correctly.

If it is possible to verify this, we can do it in a follow-up issue/PR. It doesn't need to be addressed in this PR.

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'll create follow-up issues for areas to test with native AOT enabled. Any code that tests IsDynamicCodeSupported is a good candidate.

Copy link
Member

Choose a reason for hiding this comment

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

The plan is to undo this anyways. This should be suppression free after we execute #45524. There will be new API changes that remove the use of RequestDelegateFactory.

Copy link
Member

@amcasey amcasey Jan 10, 2023

Choose a reason for hiding this comment

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

How does one add a "native AOT" test?

Edit: as discussed offline, we have yet to develop an approach, but we will probably model it on the one used by dotnet/runtime (i.e. create convenience wrappers for exes published as AOT).

@@ -328,3 +352,10 @@ private Task DisplayRuntimeException(HttpContext context, Exception ex)
return errorPage.ExecuteAsync(context);
}
}

internal record ExceptionExtensionData(string Details, IHeaderDictionary Headers, string Path, string? Endpoint, RouteValueDictionary? RouteValues);
Copy link
Member

Choose a reason for hiding this comment

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

For my learning:

In dotnet/runtime we typically don't use record because of the bloat associated with it - C# generates members you don't use/need. Do we have guidelines in dotnet/aspnetcore when to use record and when not? Or don't we care as much?

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 think we don't care as much. It's internal, so if we crack down on records in aspnetcore (we have them in other places) then it's simple enough to change.

Copy link
Member

@jkotas jkotas Jan 4, 2023

Choose a reason for hiding this comment

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

For reference, this is how much IL is generated by this single record line: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBLANgH1kigBMACAUQTBgAcNsIA7KjGJgZ0aYBEBDDHwAUAAQCMABjI8YgvBzRkAkgAkYfEjCg9sYBsz5QAnmTUatCsuKkAFAQAtF1gPyUmJWhGxMMigEoQAK5sAGp8uIEwOnrchkauAcEwYREwHACUANwAsABQeXkiAExkiaHhkdH6THF5AN55AL4Fud5sUABmfDTKZprautW1uQ25jUA==

This record will translate to about 5kB of binary footprint in the native aot image. 80% of it is dynamically unreachable code, but it is impossible for the trimmer to prove that it is unreachable because a lot of it is behind potentially reachable interfaces.

Copy link
Member Author

Choose a reason for hiding this comment

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

Impressive! Looks like we should clean up record usage in aspnetcore.

Comment on lines +216 to +218
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Values set on ProblemDetails.Extensions are supported by the default writer.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Values set on ProblemDetails.Extensions are supported by the default writer.")]
private ProblemDetails CreateProblemDetails(ErrorContext errorContext, HttpContext httpContext)
Copy link
Member

Choose a reason for hiding this comment

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

Why not let it warn until we have the real fix?

@@ -32,7 +38,14 @@ public IdentityBuilder(Type user, IServiceCollection services)
/// <param name="role">The <see cref="Type"/> to use for the roles.</param>
/// <param name="services">The <see cref="IServiceCollection"/> to attach to.</param>
public IdentityBuilder(Type user, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type role, IServiceCollection services) : this(user, services)
=> RoleType = role;
{
if (role.IsValueType)
Copy link
Member

Choose a reason for hiding this comment

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

Is this a breaking change?

Copy link
Member Author

@JamesNK JamesNK Jan 4, 2023

Choose a reason for hiding this comment

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

RoleType and UserType are later used to generate types, all of which have a where T : class constraint.

Throwing here means the error is different, and it is thrown in a different place. But a value type as the user or role would always lead to an error.

Although the constructor is public, I imagine the vast majority of people who ever use IdentityBuilder create it via this extension method:

public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services) where TUser : class

It enforces a class with its own constraint.

Copy link
Member

@davidfowl davidfowl left a comment

Choose a reason for hiding this comment

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

I'd like to see issues for:

  • The developer exception page WRT what we're doing JSON wise there
  • ProblemDetails and undoing what's there now
  • Undoing the DI changes when those other changes land
  • Doing API work in the Map* handlers to remove suppressions in the RouteEndpointDataSource

@JamesNK
Copy link
Member Author

JamesNK commented Jan 4, 2023

@JamesNK JamesNK merged commit 38cdcfd into main Jan 4, 2023
@JamesNK JamesNK deleted the jamesnk/Aot branch January 4, 2023 08:46
@ghost ghost added this to the 8.0-preview1 milestone Jan 4, 2023
@JamesNK
Copy link
Member Author

JamesNK commented Jan 4, 2023

Merged! Thanks everyone for your feedback.

@@ -4,6 +4,7 @@
<Description>Helpers for launching the SPA CLI proxy automatically when the application starts in ASP.NET MVC Core.</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsTrimmable>true</IsTrimmable>
<EnableAOTAnalyzer>false</EnableAOTAnalyzer>
<!-- This is ok since this assembly is not referenced by any application but it is loaded as a hosting startup
Copy link
Member

Choose a reason for hiding this comment

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

Which line was this comment intended to refer to?

[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(string))]
Copy link
Member

Choose a reason for hiding this comment

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

Is bool implied and/or specified elsewhere?

"Map methods that configure a RequestDelegate don't use trimmer unsafe features.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "We surface a RequiresDynamicCode in the call to the Map methods adding route handlers this EndpointDataSource. Analysis is unable to infer this. " +
"Map methods that configure a RequestDelegate don't use AOT unsafe features.")]
Copy link
Member

@amcasey amcasey Jan 10, 2023

Choose a reason for hiding this comment

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

How does one add a "native AOT" test?

Edit: as discussed offline, we have yet to develop an approach, but we will probably model it on the one used by dotnet/runtime (i.e. create convenience wrappers for exes published as AOT).

amcasey added a commit to amcasey/aspnetcore that referenced this pull request Jan 17, 2023
...introduced in dotnet#45604 now that dotnet/runtime#79425 is available.
@amcasey amcasey added area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares and removed area-runtime labels Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants