-
Notifications
You must be signed in to change notification settings - Fork 857
[AOT] Remove usage of Linq.Expressions and especially lambda compilation #4695
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
Conversation
In AOT `Expression.Lambda.Compile` method is implemented via an interpreter. So it does work, but it's relatively slow. Additionally it's not fully NativeAOT compatible yet. There are two places where it's used in the source code: * `ExceptionProcessor` uses it to access `Marshal.GetExceptionPointers` method which is not part of netstandard API. So in this case simply ifdef and on net6.0+ access the method directly (as it's visible in the net6.0 APIs) and fallback to the Expression.Lambda solution on down-level platforms. Since AOT/trimming is net6.0+ this fixes the problem. * `ActivityInstrumentationHelper` uses it to access two setters on the `Activity` class which are internal. Using reflection and then `CreateDelegate` will provide basically the same performance (a delegate which directly calls the setter) without a need for `Expression.Lambda` - so the code is simpler. Since the reflection is statically analyzable (all types are known, the property names are known) this code is fully trim and AOT compatible. There's no impact on warning count because the .NET 7 ASP.NET has Linq.Expression dependency as well. On .NET 8 though this removes all warnings from Linq.Expressions.
| PropertyInfo sourcePropertyInfo = typeof(Activity).GetProperty("Source"); | ||
| var body = Expression.Assign(Expression.Property(instance, sourcePropertyInfo), propertyValue); | ||
| return Expression.Lambda<Action<Activity, ActivitySource>>(body, instance, propertyValue).Compile(); | ||
| return (Action<Activity, ActivitySource>)typeof(Activity).GetProperty("Source") |
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.
When I spoke to @tarekgh about this, and asked why these setters aren't exposed publicly (since OpenTelemetry needs to call them), he told me that these setters only need to be called on < net7.0 runtimes. If you look at the callsites, you'll see this is true for 2 of the 3 callsites:
opentelemetry-dotnet/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs
Lines 195 to 198 in 0343116
| #if !NET7_0_OR_GREATER | |
| ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); | |
| ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); | |
| #endif |
Lines 161 to 165 in 0343116
| if (!IsNet7OrGreater) | |
| { | |
| ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); | |
| ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); | |
| } |
And in Grpc, it always calls it:
Lines 124 to 127 in 0343116
| if (activity.IsAllDataRequested) | |
| { | |
| ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); | |
| ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); |
I think the correct thing to do here is to not compile this code on net7.0+ TFMs, and then fix the 2 places above to have #if !NET7_0_OR_GREATER checks around them that don't already.
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.
@utpilla do you agree to change GRPC to not call these on net7.0+?
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.
There's an additional complication - it would require adding net7.0 as a TFM to Http and Grcp instrumentation assemblies. Http currently uses a boolean which is determined by looking at version of the System.Net.Http assembly, the trimmer would not be able to figure this out. I don't think we have a runtime property which the trimmer will know a constant value of and can be used to determine the version.
Additionally, Grpc package is currently netstandard2.0/netstandard2.1 only... we will probably have to change that anyway (to get the annotation attributes), but by default it would only get net6.0, not net7.0.
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.
+@noahfalk as an "fyi".
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.
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.
Older versions of aspnet and http client used to create the Activity objects the legacy way using new Activity. OpenTelemetry wanted to support these old versions of such platforms which needed to set Kind properly on such activity. This was the need for reflection. Aspnet and http client then moved (in .NET 7.0) to create the Activity object using ActivitySource which set the Kind correctly on the created Activity object so there was no need to do anything more with such objects or use the reflection.
I don't know gRPC scenarios. If it is same as aspnet then there is no need to call reflection on .NET 7.0+.
@cijothomas you were the one implementing that as I recall if you have more input here.
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 don't know gRPC scenarios.
It looks like it isn't following this guidance of using ActivitySource.CreateActivity/StartActivity. 😢 For either it's own Activity:
nor its copy of the HTTP System.Net.Http.HttpRequestOut Activity:
Maybe someone should log an issue on the gRPC client to move away from the "legacy way new Activity(...)" and use an ActivitySource. Then OpenTelemetry wouldn't need to use reflection here at all.
cc @JamesNK
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.
grpc-dotnet was written for .NET Core 3, and ActivitySource didn't exist then. grpc-dotnet still targets .NET Core 3 + .NET 5, but I'm going to drop the unsupported targets for .NET 8.
Grpc.Net.Client also targets netstandard2.0, but ActivitySource is in the System.Diagnostics.DiagnosticSource NuGet package.
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.
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.
utpilla
left a comment
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.
Looks good.
Things to do outside of this PR:
- Update targets for GrpcNetClient instrumentation library
- Check if GrpcNetClient instrumentation library can be updated to listen to ActivitySource on net7+ targets
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #4695 +/- ##
==========================================
- Coverage 85.14% 85.10% -0.04%
==========================================
Files 314 314
Lines 12735 12722 -13
==========================================
- Hits 10843 10827 -16
- Misses 1892 1895 +3
|
|
I don't understand the test failure - it passes locally just fine. |
|
Why doesn't this change decrease the AOT warning count? opentelemetry-dotnet/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs Line 88 in 8c7173f
Are there still usages of System.Linq.Expressions hanging around? |
It does on .NET 8. But the test runs on .NET 7 and in .NET 7 ASP.NET uses Linq.Expression is certain places which pull it in -> causing the same warnings. I verified that there are no Linq.Expression usages from OTel after this change. |
eerhardt
left a comment
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.
Thanks for fixing this, @vitek-karas. LGTM
Towards #3429
In AOT
Expression.Lambda.Compilemethod is implemented via an interpreter. So it does work, but it's relatively slow. Additionally it's not fully NativeAOT compatible yet.There are two places where it's used in the source code:
ExceptionProcessoruses it to accessMarshal.GetExceptionPointersmethod which is not part of netstandard API. So in this case simply ifdef and on net6.0+ access the method directly (as it's visible in the net6.0 APIs) and fallback to the Expression.Lambda solution on down-level platforms. Since AOT/trimming is net6.0+ this fixes the problem.ActivityInstrumentationHelperuses it to access two setters on theActivityclass which are internal. Using reflection and thenCreateDelegatewill provide basically the same performance (a delegate which directly calls the setter) without a need forExpression.Lambda- so the code is simpler. Since the reflection is statically analyzable (all types are known, the property names are known) this code is fully trim and AOT compatible.There's no impact on warning count because the .NET 7 ASP.NET has Linq.Expression dependency as well. On .NET 8 though this removes all warnings from Linq.Expressions.
@Yun-Ting @eerhardt
Merge requirement checklist
CHANGELOG.mdfiles updated for non-trivial changes