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

Partially annotate IL2026 #1958

Closed
wants to merge 1 commit into from
Closed

Conversation

kant2002
Copy link
Contributor

I cannot get rid all of them since rety logic used everywhere in the SqlConnection and SqlCommad and I think there maybe other better ways. Other places is more or less self-contained and did not pollute anything.

/cc @Wraith2

@@ -36,6 +37,9 @@ internal class DbMetaDataFactory
private const string SqlCommandKey = "SQLCommand";
private const string PrepareCollectionKey = "PrepareCollection";

#if NET6_0_OR_GREATER
Copy link
Contributor

Choose a reason for hiding this comment

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

"Metadata loaded from XML stream may require types that have been trimmed away" ?

@codecov
Copy link

codecov bot commented Mar 22, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: -6.92 ⚠️

Comparison is base (3607fe2) 70.67% compared to head (d7605d6) 63.75%.

❗ Current head d7605d6 differs from pull request most recent head 272219b. Consider uploading reports for the commit 272219b to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1958      +/-   ##
==========================================
- Coverage   70.67%   63.75%   -6.92%     
==========================================
  Files         306      306              
  Lines       61995    61555     -440     
==========================================
- Hits        43812    39247    -4565     
- Misses      18183    22308    +4125     
Flag Coverage Δ
addons 0.00% <ø> (-92.89%) ⬇️
netcore 67.09% <100.00%> (-6.32%) ⬇️
netfx 62.39% <100.00%> (-6.94%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...ity/SqlConfigurableRetryLogicManager.NetCoreApp.cs 100.00% <ø> (+2.50%) ⬆️
...SqlClient/SqlClientDiagnosticListenerExtensions.cs 61.59% <ø> (ø)
...netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs 61.59% <ø> (-22.10%) ⬇️
...core/src/Microsoft/Data/SqlClient/SqlConnection.cs 76.24% <ø> (-3.21%) ⬇️
...c/Microsoft/Data/SqlClient/SqlConnectionFactory.cs 66.39% <ø> (-2.46%) ⬇️
...c/Microsoft/Data/ProviderBase/DbMetaDataFactory.cs 84.44% <ø> (ø)
...ft/Data/Sql/SqlDataSourceEnumeratorNativeHelper.cs 88.37% <ø> (ø)
...ent/Reliability/SqlConfigurableRetryLogicLoader.cs 85.48% <ø> (-0.14%) ⬇️
...c/Microsoft/Data/SqlClient/SqlClientEventSource.cs 78.76% <ø> (-0.35%) ⬇️
...src/Microsoft/Data/SqlClient/SqlMetadataFactory.cs 92.50% <ø> (ø)
... and 2 more

... and 91 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@kant2002 kant2002 force-pushed the kant/remove-il2026 branch from d7605d6 to 368a1f1 Compare March 25, 2023 19:11
I cannot get rid all of them since rety logic used everywhere in the SqlConnection and SqlCommad and I think there maybe other better ways.
Other places is more or less self-contained and did not pollute anything.
@kant2002 kant2002 force-pushed the kant/remove-il2026 branch from 368a1f1 to 272219b Compare July 18, 2023 18:35
Comment on lines +34 to +36
// Fetch the section attributes values from the configuration section of the app config file.
cnnConfig = AppConfigManager.FetchConfigurationSection<SqlConfigurableRetryConnectionSection>(SqlConfigurableRetryConnectionSection.Name);
cmdConfig = AppConfigManager.FetchConfigurationSection<SqlConfigurableRetryCommandSection>(SqlConfigurableRetryCommandSection.Name);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Wraith2 starting from this two lines we apply RequiresUnreferencedCode across codebase. this bubble up to override of DbDataReader like ExecuteScalar/ExecuteDbDataReader, etc. In .NET DbDataReader does not marked with RUC so it is prohibited to mark these methods with RUC in derived classes.

I'm not sure that App.config code path is actually excercised on .NET variant, but if so, I really would like to have different configuration for that. if no configuration in app.config then only one instance of class is available here, so I really would like to streamline instantiation of that class without using all these "reflection" magic somehow.

Copy link
Member

Choose a reason for hiding this comment

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

Assuming it's uncommon to use the config file to provide a custom class, this whole thing might be a candidate for a feature switch.

Basically disable this capability by default for trimmed apps. If the user enables it explicitly via a property in the csproj file, they would get a warning and would have to deal with it on their own.

But this requires somebody with the knowledge of how this is used - the feature switches are a reasonable solution only for rarely used capabilities. They also degrade the overall experience - instead of providing compile time warnings about trim incompatibilities, the failure will happen only at runtime, so they should be used only when other solutions are not viable.

@@ -518,6 +521,8 @@ private static string Format(FormattableString s)
#region Custom WriteEvent overloads

[NonEvent]
[SuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern",
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be UnconditionalSuppressMessage? It's available on .NET 5+ so it needs an #if guard.

Copy link
Member

Choose a reason for hiding this comment

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

Yes - it MUST be UnconditionalSuppressMessage. SuppressMessage is "conditional" and the compiler will not emit it to IL - it disappears after compilation. As a result the trimmer doesn't get to see it.

We've been discussing some possibilities to provide an analyzer for these "bugs" but it's difficult (SuppressMessage is handled by the compiler intrinsically...). I could not find a tracking issue, so I created dotnet/runtime#93453.

@JRahnama JRahnama added this to the 5.2.0-preview4 milestone Aug 15, 2023
@David-Engel David-Engel removed the request for review from cheenamalhotra August 22, 2023 18:38
@@ -14,6 +15,8 @@ namespace Microsoft.Data.SqlClient
/// </summary>
internal static class SqlClientDiagnosticListenerExtensions
{
private const string EventSourceSuppressMessage = "Parameters to this method are statically known and thus are trimmer safe";
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain why suppression is required while the parameters are genuinely trimmer-safe? I expect the analyzer to distinguish it. VS recognizes them as an unnecessary suppression that could be removed though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IL trimmer made design decision operate on IL level, when it see virtual call he don't perform full application analysys to prove that this is actuall specific call. So each virtual call should have same annotations as base class.

I know, probably that can be relaxed, but it is how it is. @vitek-karas maybe give more perspective why this is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ooh, here. I should not be that fast in answering. EventSource.Write annotated as RequireUnreferencedCode, and trimmer think that if somebody use such methods it's dangerous. It is expected suppress warning like that

Copy link
Member

Choose a reason for hiding this comment

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

This is actually not correct and should not be suppressed as-is. This code for example means that OpenTelemetry's SqlClient instrumentation is not trim/AOT compatible. https://github.com/open-telemetry/opentelemetry-dotnet/blob/b091af1899b66a516d753109ce6bf8182be416e0/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs#L56C9-L61

Sorry it fell through cracks on my side, I meant to file an issue around this long time ago... so I finally did it: #2184. Other libraries are in similar (although not as bad) problem, so we're slowly trying to get this fixed. OTel in its latest version is now fully AOT compatible, with the exception of the SqlClient instrumentation as there's no good way to make that work correctly right now.

As described in the issue - the best solution is to declare all of these payload types as public. That way the consumer (OTel for example) can just cast they payload instance to that type and directly access its properties. That would also fix all of the trimming issues related to in-proc consumers which are using the public types.

Separate issue is all the consumers which are either using reflection or rely on diagnostic event source to do the same. For those the only solution is to explicitely hint the trimmer to preserve certain properties. For example System.Http, ASP.NET and GRPC all do this to some degree. For example HTTP stack:
https://github.com/dotnet/runtime/blob/9c4d51c0a45a129745b2eec9fb5497042cd805fb/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 - this preserves all top-level properties on the payload object
https://github.com/dotnet/runtime/blob/9c4d51c0a45a129745b2eec9fb5497042cd805fb/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L272-L276 - this preserves some nested properties on one of the payload objects.

This is not a precise solution and never will be, but it's worth preserving at least properties used by OTel and other popular consumers (personally I don't know what they would be though).

Comment on lines +301 to +305
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GroupByBehavior))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IdentifierCase))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SupportedJoinOperators))]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All non-primitive types which are contained in the XML stream marked as dynamic dependencies.")]
Copy link
Member

Choose a reason for hiding this comment

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

In cases like these we would generally recommend adding a unit test which validates the assumption here. So in this case a test which would deserialize the embedded XML and then walk the returned object graph (using reflection) and check that it only encountered expected set of types. You could go even the final step and read the expected set of types from the DynamicDependencyAttribute on this method.

The main goal of such a test is to make the code maintainable. If in the future somebody changes the XML and adds a new type into it, the assumption here would break, but there would be no way to tell (nothing would produce a warning). So the unit test acts as the guard of that.

We've done things like this in other repos as well - and it seems to work pretty well.

Comment on lines +301 to +303
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GroupByBehavior))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IdentifierCase))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SupportedJoinOperators))]
Copy link
Member

Choose a reason for hiding this comment

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

Is it really required to have All? All is weird in that it applies recursively to all base types and interfaces, so it can bring in a LOT of dependencies. If we're just reading these from XML, maybe some like "public/private properties, constructors and fields" may be enough?

@@ -121,6 +121,9 @@ private enum CultureCheckState : uint
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public SqlRetryLogicBaseProvider RetryLogicProvider
{
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("RetryLogicProvider can be read from app.config which is unsafe for trimming")]
Copy link
Member

Choose a reason for hiding this comment

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

Typical pattern is to define a const string field with the message and the use that. It makes it easy to improve the message and make it consistent across multiple usages. It also makes it clearer if the RequiresUnreferencedCode is "propagated" across multiple methods in a call chain - that they all belong together and are for the same reason.

@@ -495,6 +499,9 @@ private bool IncludeThisColumn(DataColumn sourceColumn, string[] hiddenColumnNam
return result;
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("LoadDataSetFromXml uses System.Data.DataSet.ReadXml(Stream)")]
Copy link
Member

Choose a reason for hiding this comment

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

In general would avoid using messages like this - the message should be telling the caller of this method what is the problem. Describing that the problem is that this method is going to call another method is not very helpful to the caller.

I know that these are private methods, so it doesn't matter much, but that's only true now. 6 months from now if somebody adds a call to this method they will get a warning with this message. And it would effectively require them to go search through the code and across multiple methods to determine the real problem. It's also a good habit, to avoid doing this on a public API.

Comment on lines +110 to +113
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Filter expression uses only primitive types and trimmer safe")]
#endif
Copy link
Member

Choose a reason for hiding this comment

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

I would recommend avoiding suppressions on large methods. These are problematic because:

  • The suppression applies to the entire method body. It is typically intended to suppress one or two specific calls, but it covers everything. Later on if somebody adds a new code into the method, and introduces a new trim incompatibility, the suppression will still silence that warning as well - and the justification may be wrong at that time -> introducing a trim incompatibility problem.
  • It's hard to read - for example I still can't tell exactly where the suppression applies and which primitive types it's talking about.

I would suggest to refactor the code such that the problematic call is isolated into a local function and the suppression is added onto the local function.

I know it makes the code a little less clean, but it's the price we have to pay for the fact that there's no way to add an attribute (or really any kind of metadata) onto a block of IL instructions.

Comment on lines +20 to +21
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Usages of this property annotated with RequiresUnreferencedCode")]
Copy link
Member

Choose a reason for hiding this comment

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

This feels wrong - if the usages are all annotated, then why not simply mark this with RequiresUnreferencedCode?

Also of note - this suppression will not work in the trimmer. The initialization code is actually part of the static constructor for the class in IL. This is a long standing problem we haven't been able to fix yet.

The simplest solution is probably to add an explicit static constructor, initialize the values there and suppress the warnings there as well.

Comment on lines +61 to +62
[SuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern",
Justification = "DataContractJsonSerializer used here deserialize only byte[] which is already kept.")]
Copy link
Member

Choose a reason for hiding this comment

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

Use the local function refactoring to apply the suppression only onto the specific piece of code (in this case the two lines inside the using statement)

@David-Engel
Copy link
Contributor

@kant2002 Can you address vitek-karas' comments?

I also recommend pulling/merging from main, too, as there is a fix that is needed to avoid validation pipeline failures now.

@JRahnama JRahnama removed this from the 5.2.0-preview4 milestone Nov 21, 2023
@cheenamalhotra
Copy link
Member

Closing stale PRs, please open a new PR when ready with feedback implemented.

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.

8 participants