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

Trimming for .NET 7+ #2699

Closed
wants to merge 21 commits into from
Closed

Conversation

jamescrosswell
Copy link
Collaborator

@jamescrosswell jamescrosswell commented Oct 5, 2023

Summary

The goal is to support AOT on .NET 7 and 8, per #2247. Matt has left quite a bit of context in the Issue already.

Additional Reference material:

Trimming issues

When enabling trimming for Sentry.csproj we have the following main issues.

Ben.Demystifier ✅

Ben.Demystifier uses reflection heavily to provide enhanced stack traces.

Approach / Solution

We've disable Ben.Demystifier (so only original stack traces) if IsTrimmable == true.

WinUIUnhandledExceptionIntegration ✅

WinUIUnhandledExceptionIntegration uses reflection to set Microsoft.UI.Xaml.Application.Current.UnhandledException to Sentry's WinUIUnhandledExceptionHandler for customers using Microsoft.UI.Xaml (.NET MAUI targeting Windows or WinUI 3 applications). The relection code it users generates the following errors with IsTrimming==true:

         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(84,35): error IL2026: Using member 'System.Reflection.Assembly.GetType(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.
         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(85,31): error IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(86,29): error IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicEvents' in call to 'System.Type.GetEvent(String)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(84,35): error IL2026: Using member 'System.Reflection.Assembly.GetType(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.
         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(85,31): error IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
         src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs(86,29): error IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicEvents' in call to 'System.Type.GetEvent(String)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Brainstorming / Options

  • Move WinUIUnhandledExceptionIntegration to the Sentry.Maui package, to a new package, or make a Windows target for the main Sentry package...
    • All of these options would require us to setup a separate CI process to build/pack the Nuget packages where WinUIUnhandledExceptionIntegration gets registered as the event handler
  • See if DynamicDependencyAttribute will let us keep it as-is without it being trimmed out.
    • That might be an option for the IL2075 but I don't think it will work for the IL2026 error
  • Change our guidance in the docs for WinUI users to have them wire that up manually, if they're using AOT
  • Source Generator solutions?

Approach / Solution

  • For the time being, if IsTrimmable == true we're not registering our WinUiUnhandledExceptionHandler automatically and we'll need to provide some guidance to users who are both targeting WinUI and wanting to enable AOT compilation.

System.Text.Json.JsonSerializer ☣️

We're still getting the following warnings/errors, which we haven't yet resolved.

         src/Sentry/GraphQLRequestContent.cs(28,32): error IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize<TValue>(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
         src/Sentry/Internal/Extensions/JsonExtensions.cs(484,17): error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(Utf8JsonWriter, TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
         src/Sentry/Internal/Extensions/JsonExtensions.cs(493,17): error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(Utf8JsonWriter, TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
         src/Sentry/Internal/Extensions/JsonExtensions.cs(500,17): error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(Utf8JsonWriter, TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.

Brainstorming / Options

The most obvious solution is to try to use System.Text.Json source generators (as the error messages suggest). This should, at the very least, work for known types. It might get a little fiddly for some of the classes we've got with Dictionary<string,object?> properties... need to play around with this to get a feel for it though. As Matt suggested, we could consider an API that would let the end-user provide JsonSerializerContext.

Approach / Solution

We have exposed an API for users to supply a custom JsonSerializerContext via SentryOptions.AddJsonSerializerContext.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 5, 2023

Fails
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

- Trimming for .NET 7+ ([#2699](https://github.com/getsentry/sentry-dotnet/pull/2699))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description.

Generated by 🚫 dangerJS against 89684a2

@vaind
Copy link
Collaborator

vaind commented Oct 5, 2023

We've disable Ben.Demystifier (so only original stack traces) if IsTrimmable == true. This is a bummer and maybe we can find a way to get enhanced stack traces again later (once we've worked through all the other issues). Short term, this is the easiest fix though.

I'm not sure Ben.Demystifier would make much sense in Native AOT because there is not enough information at runtime. Most we can get is a module (ns+class) & method name parsed from frame ToString() output. See dotnet/runtime#92869

@jamescrosswell
Copy link
Collaborator Author

I'm not sure Ben.Demystifier would make much sense in Native AOT because there is not enough information at runtime.

OK phew! That's one less thing on the backlog then 😃

@jamescrosswell jamescrosswell self-assigned this Oct 9, 2023
Copy link
Collaborator

@vaind vaind left a comment

Choose a reason for hiding this comment

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

I've come to look at this PR to see how you're checking whether an app is being published as Native AOT. As far as I can tell, it can only be done at runtime with our current packaging process because we're only shipping a single compiled DLL so we can't just ifdef.

Alternatively, I think we could ship two versions of the library (for each framework), by following the approach given here: https://learn.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks#architecture-specific-folders and have a version for aot platform identifier and another for any.

src/Sentry/Internal/DebugStackTrace.cs Outdated Show resolved Hide resolved
@@ -5,6 +5,8 @@
<NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0'">$(NoWarn);RS0017</NoWarn>
<CLSCompliant Condition="'$(TargetPlatformIdentifier)' == ''">true</CLSCompliant>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsTrimmable>true</IsTrimmable>
<DefineConstants>$(DefineConstants);TRIMMABLE</DefineConstants>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't this add the TRIMMABLE constant unconditionally? That effectively removes enhanced stack frames at all times.

Copy link
Collaborator Author

@jamescrosswell jamescrosswell Oct 11, 2023

Choose a reason for hiding this comment

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

I does yeah... I didn't intend for this to be merged into the feat/4.0.0 branch - it was just a quick/easy hack to enable trimming so that I could get a sense for the challenges and start to try to work through those.

@vaind vaind mentioned this pull request Oct 10, 2023
4 tasks
@jamescrosswell
Copy link
Collaborator Author

jamescrosswell commented Oct 11, 2023

I've come to look at this PR to see how you're checking whether an app is being published as Native AOT. As far as I can tell, it can only be done at runtime with our current packaging process because we're only shipping a single compiled DLL so we can't just ifdef.

Alternatively, I think we could ship two versions of the library (for each framework), by following the approach given here: https://learn.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks#architecture-specific-folders and have a version for aot platform identifier and another for any.

I was scratching my head to think how we could do this as well and haven't come up with any good options yet. We might, as you suggested, need separate packages for AOT and Managed.

I'm not sure how that is done in MSBuild though. When targeting multiple frameworks, there's a TargetFrameworks property... but I don't know of the equivalent for PlatformTarget.

We can always pass this in via an environment variable or the command line, which will work well for our CI workflows on GitHub, but that seems like a clumsy experience for us on our development machines... particularly for testing (where again, multiple frameworks are well supported but multiple platforms conspicuously not mentioned 🤔).

@jamescrosswell jamescrosswell changed the title WIP: AOT for .NET 7+ WIP: Trimming for .NET 7+ Oct 11, 2023
@jamescrosswell jamescrosswell added this to the 4.0.0 milestone Oct 11, 2023
@vaind
Copy link
Collaborator

vaind commented Oct 14, 2023

particularly for testing (where again, multiple frameworks are well supported but multiple platforms conspicuously not mentioned 🤔).

I've already checked this a couple of weeks ago. There's no support for unit testing at the moment, see dotnet/runtime#79316. Also citing a response from the thread:

If there are no warnings, the code should work and there's no need to do separate functional testing with trimming or AOT.

On the other hand, I think we need to do integration tests ourselves (for things we can't mock in unit tests) because of the different handling of native-aot vs "standard" builds.

@jamescrosswell
Copy link
Collaborator Author

If there are no warnings, the code should work and there's no need to do separate functional testing with trimming or AOT.

Enabling trimming will likely be a more constrained/limited experience. We can't serialize anonymous types, enhanced stack traces won't work with AOT and there may be other differences. Users who don't need AOT would probably miss the flexibility/capability they get from reflection so I suspect we'll have to maintain variants of the codebase depending on whether trimming is enabled or not.

On the other hand, I think we need to do integration tests ourselves (for things we can't mock in unit tests) because of the different handling of native-aot vs "standard" builds.

Yeah, if we support both AOT and Managed, at the very least we'll want to test each assembly with trimming enabled and disabled. Currently it looks like Sentry will be behave quite different when Trimming/AOT is enabled.

@vaind
Copy link
Collaborator

vaind commented Oct 14, 2023

We can't serialize anonymous types,

If you mean the dynamic-value stuff in json extensions - can't we get rid of that completely, regardless of publishing type? There was a breaking change suggested by Matt in the issue about requiring an interface instead?

@jamescrosswell
Copy link
Collaborator Author

If you mean the dynamic-value stuff in json extensions - can't we get rid of that completely, regardless of publishing type?

Maybe. We'd have to work out what to do with things like Span.Extra and Transaction.Extra, in that case:

public IReadOnlyDictionary<string, object?> Extra => _extra;

Currently users can put arbitrary things in the scope with Scope.SetExtra(string key, object? value) and our JSON serializers can use reflection to magic it all out, without the value parameter having to implement any interface or anything... which doesn't work with trimming of course (due to the reflection).

At the moment, the code I have preserves our previous functionality if people don't have trimming enabled... so the scenario above is only a problem when Trimming is turned on.

Either way, we have to solve the above for the trimming scenario and maybe then we see whether that behaviour is acceptable in managed/JIT scenarios as well. I'll play around with it over the next few days.

@jamescrosswell jamescrosswell changed the title WIP: Trimming for .NET 7+ Blocked: Trimming for .NET 7+ Oct 16, 2023
@jamescrosswell jamescrosswell changed the title Blocked: Trimming for .NET 7+ Trimming for .NET 8+ Oct 17, 2023
@jamescrosswell jamescrosswell changed the title Trimming for .NET 8+ Trimming for .NET 7+ Oct 17, 2023
@jamescrosswell
Copy link
Collaborator Author

Closing in favour of #2732

@jamescrosswell jamescrosswell deleted the feat/4.0.0-sentry-trimmable branch October 18, 2023 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants