Skip to content

Commit

Permalink
Resolve AOT compilation issues (#1737)
Browse files Browse the repository at this point in the history
- Fix #1732 using `RuntimeFeature.IsDynamicCodeSupported` to guard against the allocation regression that avoiding the infinite generic recursion causes through boxing.
- Add a console project that validates whether the new-in-v8 Polly assemblies are AoT compatible.
- Mark the new Polly v8 assemblies as AoT compatible.
- Add a micro-benchmark for `DelegatingComponent` code path for non-AoT and AoT.
  • Loading branch information
martincostello authored Oct 30, 2023
1 parent 61fdcbd commit e404873
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* Speed-up page builds by [@martincostello](https://github.com/martincostello) in https://github.com/App-vNext/Polly/pull/1753
* Hedging strategy also deep-copies context for primary execution by [@martintmk](https://github.com/martintmk) in https://github.com/App-vNext/Polly/pull/1754
* [Docs] Add diagram about hedging's context and callbacks by [@peter-csala](https://github.com/peter-csala) in https://github.com/App-vNext/Polly/pull/1751
* Resolve AOT compilation issues by [@martincostello](https://github.com/martincostello) in https://github.com/App-vNext/Polly/pull/1737

## 8.0.0

Expand Down
7 changes: 7 additions & 0 deletions Polly.sln
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing.Tests", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snippets", "src\Snippets\Snippets.csproj", "{D812B941-79B0-4E1E-BB70-4FAE345B5234}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.AotTest", "test\Polly.AotTest\Polly.AotTest.csproj", "{84091007-CFA5-4852-AC41-0171DF039C4E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -120,6 +122,10 @@ Global
{D812B941-79B0-4E1E-BB70-4FAE345B5234}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.Build.0 = Release|Any CPU
{84091007-CFA5-4852-AC41-0171DF039C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84091007-CFA5-4852-AC41-0171DF039C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84091007-CFA5-4852-AC41-0171DF039C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84091007-CFA5-4852-AC41-0171DF039C4E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -140,6 +146,7 @@ Global
{9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9} = {B7BF406B-B06F-4025-83E6-7219C53196A6}
{D333B5CE-982D-4C11-BDAF-4217AA02306E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB}
{D812B941-79B0-4E1E-BB70-4FAE345B5234} = {B7BF406B-B06F-4025-83E6-7219C53196A6}
{84091007-CFA5-4852-AC41-0171DF039C4E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E5D54CD-770A-4345-B585-1848FC2EA6F4}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ LaunchCount=2 WarmupCount=10
```
| Method | Mean | Error | StdDev | Allocated |
|------------------------------- |---------:|---------:|---------:|----------:|
| CompositeComponent_ExecuteCore | 44.37 ns | 1.994 ns | 2.923 ns | - |
| CompositeComponent_ExecuteCore | 37.44 ns | 0.713 ns | 0.952 ns | - |
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```
BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 7.0.403
[Host] : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2
Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=2 WarmupCount=10
```
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------------------------------ |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:|
| DelegatingComponent_ExecuteCore_Jit | 29.40 ns | 0.699 ns | 1.025 ns | 1.00 | 0.00 | - | - | NA |
| DelegatingComponent_ExecuteCore_Aot | 34.65 ns | 0.627 ns | 0.919 ns | 1.18 | 0.05 | 0.0025 | 24 B | NA |
38 changes: 38 additions & 0 deletions bench/Polly.Core.Benchmarks/DelegatingComponentBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Polly.Utils.Pipeline;

namespace Polly.Core.Benchmarks;

public class DelegatingComponentBenchmark : IAsyncDisposable
{
private ResilienceContext? _context;
private DelegatingComponent? _component;

[GlobalSetup]
public void Setup()
{
var first = PipelineComponent.Empty;
var second = PipelineComponent.Empty;

_component = new DelegatingComponent(first) { Next = second };
_context = ResilienceContextPool.Shared.Get();
}

public async ValueTask DisposeAsync()
{
if (_component is not null)
{
await _component.DisposeAsync().ConfigureAwait(false);
_component = null;
}

GC.SuppressFinalize(this);
}

[Benchmark(Baseline = true)]
public ValueTask<Outcome<int>> DelegatingComponent_ExecuteCore_Jit()
=> _component!.ExecuteComponent((_, state) => Outcome.FromResultAsValueTask(state), _context!, 42);

[Benchmark]
public ValueTask<Outcome<int>> DelegatingComponent_ExecuteCore_Aot()
=> _component!.ExecuteComponentAot((_, state) => Outcome.FromResultAsValueTask(state), _context!, 42);
}
28 changes: 23 additions & 5 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ Task("__Clean")

CleanDirectories(cleanDirectories);

foreach(var path in cleanDirectories) { EnsureDirectoryExists(path); }
foreach (var path in cleanDirectories) { EnsureDirectoryExists(path); }

foreach(var path in solutionPaths)
foreach (var path in solutionPaths)
{
Information("Cleaning {0}", path);

Expand All @@ -93,7 +93,7 @@ Task("__Clean")
Task("__RestoreNuGetPackages")
.Does(() =>
{
foreach(var solution in solutions)
foreach (var solution in solutions)
{
Information("Restoring NuGet Packages for {0}", solution);
DotNetRestore(solution.ToString());
Expand All @@ -103,7 +103,7 @@ Task("__RestoreNuGetPackages")
Task("__BuildSolutions")
.Does(() =>
{
foreach(var solution in solutions)
foreach (var solution in solutions)
{
Information("Building {0}", solution);

Expand All @@ -125,6 +125,23 @@ Task("__BuildSolutions")
}
});

Task("__ValidateAot")
.Does(() =>
{
var aotProject = MakeAbsolute(File("./test/Polly.AotTest/Polly.AotTest.csproj"));
var settings = new DotNetPublishSettings
{
Configuration = configuration,
Verbosity = DotNetVerbosity.Minimal,
MSBuildSettings = new DotNetMSBuildSettings
{
TreatAllWarningsAs = MSBuildTreatAllWarningsAs.Error,
},
};

DotNetPublish(aotProject.ToString(), settings);
});

Task("__RunTests")
.Does(() =>
{
Expand All @@ -137,7 +154,7 @@ Task("__RunTests")

var projects = GetFiles("./test/**/*.csproj");

foreach(var proj in projects)
foreach (var proj in projects)
{
DotNetTest(proj.FullPath, new DotNetTestSettings
{
Expand Down Expand Up @@ -252,6 +269,7 @@ Task("Build")
.IsDependentOn("__RestoreNuGetPackages")
.IsDependentOn("__ValidateDocs")
.IsDependentOn("__BuildSolutions")
.IsDependentOn("__ValidateAot")
.IsDependentOn("__RunTests")
.IsDependentOn("__RunMutationTests")
.IsDependentOn("__CreateNuGetPackages");
Expand Down
1 change: 1 addition & 0 deletions src/Polly.Core/Polly.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsAotCompatible>true</IsAotCompatible>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Expand Down
33 changes: 0 additions & 33 deletions src/Polly.Core/Utils/Pipeline/CompositeComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,37 +116,4 @@ private async ValueTask<Outcome<TResult>> ExecuteCoreWithTelemetry<TResult, TSta

return outcome;
}

/// <summary>
/// A component that delegates the execution to the next component in the chain.
/// </summary>
private sealed class DelegatingComponent : PipelineComponent
{
private readonly PipelineComponent _component;

public DelegatingComponent(PipelineComponent component) => _component = component;

public PipelineComponent? Next { get; set; }

internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
return _component.ExecuteCore(
static (context, state) =>
{
if (context.CancellationToken.IsCancellationRequested)
{
return Outcome.FromExceptionAsValueTask<TResult>(new OperationCanceledException(context.CancellationToken).TrySetStackTrace());
}

return state.Next!.ExecuteCore(state.callback, context, state.state);
},
context,
(Next, callback, state));
}

public override ValueTask DisposeAsync() => default;
}
}
82 changes: 82 additions & 0 deletions src/Polly.Core/Utils/Pipeline/DelegatingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Polly.Utils.Pipeline;

/// <summary>
/// A component that delegates the execution to the next component in the chain.
/// </summary>
internal sealed class DelegatingComponent : PipelineComponent
{
private readonly PipelineComponent _component;

public DelegatingComponent(PipelineComponent component) => _component = component;

public PipelineComponent? Next { get; set; }

public override ValueTask DisposeAsync() => default;

[ExcludeFromCodeCoverage]
internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
#if NET6_0_OR_GREATER
return RuntimeFeature.IsDynamicCodeSupported ? ExecuteComponent(callback, context, state) : ExecuteComponentAot(callback, context, state);
#else
return ExecuteComponent(callback, context, state);
#endif
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ValueTask<Outcome<TResult>> ExecuteNext<TResult, TState>(
PipelineComponent next,
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
if (context.CancellationToken.IsCancellationRequested)
{
return Outcome.FromExceptionAsValueTask<TResult>(new OperationCanceledException(context.CancellationToken).TrySetStackTrace());
}

return next.ExecuteCore(callback, context, state);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueTask<Outcome<TResult>> ExecuteComponent<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
return _component.ExecuteCore(
static (context, state) => ExecuteNext(state.Next!, state.callback, context, state.state),
context,
(Next, callback, state));
}

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueTask<Outcome<TResult>> ExecuteComponentAot<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
// Custom state object is used to cast the callback and state to prevent infinite
// generic type recursion warning IL3054 when referenced in a native AoT application.
// See https://github.com/App-vNext/Polly/issues/1732 for further context.
return _component.ExecuteCore(
static (context, wrapper) =>
{
var callback = (Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>>)wrapper.Callback;
var state = (TState)wrapper.State;
return ExecuteNext(wrapper.Next, callback, context, state);
},
context,
new StateWrapper(Next!, callback, state!));
}

private readonly record struct StateWrapper(PipelineComponent Next, object Callback, object State);
#endif
}
2 changes: 1 addition & 1 deletion src/Polly.Core/Utils/Pipeline/PipelineComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal Outcome<TResult> ExecuteCoreSync<TResult, TState>(

public abstract ValueTask DisposeAsync();

private class NullComponent : PipelineComponent
private sealed class NullComponent : PipelineComponent
{
internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, ResilienceContext context, TState state)
=> callback(context, state);
Expand Down
1 change: 1 addition & 0 deletions src/Polly.Extensions/Polly.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsAotCompatible>true</IsAotCompatible>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Polly.RateLimiting/Polly.RateLimiting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsAotCompatible>true</IsAotCompatible>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Expand Down
19 changes: 19 additions & 0 deletions test/Polly.AotTest/Polly.AotTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<SKIP_POLLY_ANALYZERS>true</SKIP_POLLY_ANALYZERS>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Polly.Core\Polly.Core.csproj" />
<ProjectReference Include="..\..\src\Polly.Extensions\Polly.Extensions.csproj" />
<ProjectReference Include="..\..\src\Polly.RateLimiting\Polly.RateLimiting.csproj" />
</ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="Polly.Core" />
<TrimmerRootAssembly Include="Polly.Extensions" />
<TrimmerRootAssembly Include="Polly.RateLimiting" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions test/Polly.AotTest/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Console.WriteLine("Hello Polly!");
Loading

0 comments on commit e404873

Please sign in to comment.