-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Docs] Introduce Performance article (#1600)
- Loading branch information
Showing
8 changed files
with
279 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Performance | ||
|
||
Polly is fast and avoids allocations wherever possible. We use a comprehensive set of [performance benchmarks](https://github.com/App-vNext/Polly/tree/main/bench/Polly.Core.Benchmarks) to monitor Polly's performance. | ||
|
||
Here's an example of results from an advanced pipeline composed of the following strategies: | ||
|
||
- Timeout (outer) | ||
- Rate limiter | ||
- Retry | ||
- Circuit breaker | ||
- Timeout (inner) | ||
|
||
--- | ||
|
||
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | | ||
| ------------------- | -------: | --------: | --------: | ----: | ------: | -----: | --------: | ----------: | | ||
| Execute policy v7 | 2.277 μs | 0.0133 μs | 0.0191 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 | | ||
| Execute pipeline v8 | 2.089 μs | 0.0105 μs | 0.0157 μs | 0.92 | 0.01 | - | 40 B | 0.01 | | ||
|
||
--- | ||
|
||
Compared to older versions, Polly v8 is both faster and more memory efficient. | ||
|
||
## Performance tips | ||
|
||
If you're aiming for the best performance with Polly, consider these tips: | ||
|
||
### Use static lambdas | ||
|
||
Lambdas capturing variables from their outer scope will allocate on every execution. Polly provides tools to avoid this overhead, as shown in the example below: | ||
|
||
<!-- snippet: perf-lambdas --> | ||
```cs | ||
// This call allocates for each invocation since the "userId" variable is captured from the outer scope. | ||
await resiliencePipeline.ExecuteAsync( | ||
cancellationToken => GetMemberAsync(userId, cancellationToken), | ||
cancellationToken); | ||
|
||
// This approach uses a static lambda, avoiding allocations. | ||
// The "userId" is stored as state, and the lambda reads it. | ||
await resiliencePipeline.ExecuteAsync( | ||
static (state, cancellationToken) => GetMemberAsync(state, cancellationToken), | ||
userId, | ||
cancellationToken); | ||
``` | ||
<!-- endSnippet --> | ||
|
||
### Use switch expressions for predicates | ||
|
||
The `PredicateBuilder` maintains a list of all registered predicates. To determine whether the results should be processed, it iterates through this list. Using switch expressions can help you bypass this overhead. | ||
|
||
<!-- snippet: perf-switch-expressions --> | ||
```cs | ||
// Here, PredicateBuilder is used to configure which exceptions the retry strategy should handle. | ||
new ResiliencePipelineBuilder() | ||
.AddRetry(new() | ||
{ | ||
ShouldHandle = new PredicateBuilder() | ||
.Handle<SomeExceptionType>() | ||
.Handle<InvalidOperationException>() | ||
.Handle<HttpRequestException>() | ||
}) | ||
.Build(); | ||
|
||
// For optimal performance, it's recommended to use switch expressions over PredicateBuilder. | ||
new ResiliencePipelineBuilder() | ||
.AddRetry(new() | ||
{ | ||
ShouldHandle = args => args.Outcome.Exception switch | ||
{ | ||
SomeExceptionType => PredicateResult.True(), | ||
InvalidOperationException => PredicateResult.True(), | ||
HttpRequestException => PredicateResult.True(), | ||
_ => PredicateResult.False() | ||
} | ||
}) | ||
.Build(); | ||
``` | ||
<!-- endSnippet --> | ||
|
||
### Execute callbacks without throwing exceptions | ||
|
||
Polly provides the `ExecuteOutcomeAsync` API, returning results as `Outcome<T>`. The `Outcome<T>` might contain an exception instance, which you can check without it being thrown. This is beneficial when employing exception-heavy resilience strategies, like circuit breakers. | ||
|
||
<!-- snippet: perf-execute-outcome --> | ||
```cs | ||
// Execute GetMemberAsync and handle exceptions externally. | ||
try | ||
{ | ||
await pipeline.ExecuteAsync(cancellationToken => GetMemberAsync(id, cancellationToken), cancellationToken); | ||
} | ||
catch (Exception e) | ||
{ | ||
// Log the exception here. | ||
logger.LogWarning(e, "Failed to get member with id '{id}'.", id); | ||
} | ||
|
||
// The example above can be restructured as: | ||
// Acquire a context from the pool | ||
ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); | ||
|
||
// Instead of wrapping pipeline execution with try-catch, use ExecuteOutcomeAsync(...). | ||
// Certain strategies are optimized for this method, returning an exception instance without actually throwing it. | ||
Outcome<Member> outcome = await pipeline.ExecuteOutcomeAsync( | ||
static async (context, state) => | ||
{ | ||
// The callback for ExecuteOutcomeAsync must return an Outcome<T> instance. Hence, some wrapping is needed. | ||
try | ||
{ | ||
return Outcome.FromResult(await GetMemberAsync(state, context.CancellationToken)); | ||
} | ||
catch (Exception e) | ||
{ | ||
return Outcome.FromException<Member>(e); | ||
} | ||
}, | ||
context, | ||
id); | ||
|
||
// Handle exceptions using the Outcome<T> instance instead of try-catch. | ||
if (outcome.Exception is not null) | ||
{ | ||
logger.LogWarning(outcome.Exception, "Failed to get member with id '{id}'.", id); | ||
} | ||
|
||
// Release the context back to the pool | ||
ResilienceContextPool.Shared.Return(context); | ||
``` | ||
<!-- endSnippet --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
using System.Net.Http; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Snippets.Docs.Utils; | ||
|
||
namespace Snippets.Docs; | ||
|
||
internal static class Performance | ||
{ | ||
public static async Task Lambda() | ||
{ | ||
var resiliencePipeline = ResiliencePipeline.Empty; | ||
var userId = string.Empty; | ||
var cancellationToken = CancellationToken.None; | ||
|
||
#region perf-lambdas | ||
|
||
// This call allocates for each invocation since the "userId" variable is captured from the outer scope. | ||
await resiliencePipeline.ExecuteAsync( | ||
cancellationToken => GetMemberAsync(userId, cancellationToken), | ||
cancellationToken); | ||
|
||
// This approach uses a static lambda, avoiding allocations. | ||
// The "userId" is stored as state, and the lambda reads it. | ||
await resiliencePipeline.ExecuteAsync( | ||
static (state, cancellationToken) => GetMemberAsync(state, cancellationToken), | ||
userId, | ||
cancellationToken); | ||
|
||
#endregion | ||
} | ||
|
||
public static async Task SwitchExpressions() | ||
{ | ||
#region perf-switch-expressions | ||
|
||
// Here, PredicateBuilder is used to configure which exceptions the retry strategy should handle. | ||
new ResiliencePipelineBuilder() | ||
.AddRetry(new() | ||
{ | ||
ShouldHandle = new PredicateBuilder() | ||
.Handle<SomeExceptionType>() | ||
.Handle<InvalidOperationException>() | ||
.Handle<HttpRequestException>() | ||
}) | ||
.Build(); | ||
|
||
// For optimal performance, it's recommended to use switch expressions over PredicateBuilder. | ||
new ResiliencePipelineBuilder() | ||
.AddRetry(new() | ||
{ | ||
ShouldHandle = args => args.Outcome.Exception switch | ||
{ | ||
SomeExceptionType => PredicateResult.True(), | ||
InvalidOperationException => PredicateResult.True(), | ||
HttpRequestException => PredicateResult.True(), | ||
_ => PredicateResult.False() | ||
} | ||
}) | ||
.Build(); | ||
|
||
#endregion | ||
} | ||
|
||
public static async Task ExecuteOutcomeAsync() | ||
{ | ||
var pipeline = ResiliencePipeline.Empty; | ||
var cancellationToken = CancellationToken.None; | ||
var logger = NullLogger.Instance; | ||
var id = "id"; | ||
|
||
#region perf-execute-outcome | ||
|
||
// Execute GetMemberAsync and handle exceptions externally. | ||
try | ||
{ | ||
await pipeline.ExecuteAsync(cancellationToken => GetMemberAsync(id, cancellationToken), cancellationToken); | ||
} | ||
catch (Exception e) | ||
{ | ||
// Log the exception here. | ||
logger.LogWarning(e, "Failed to get member with id '{id}'.", id); | ||
} | ||
|
||
// The example above can be restructured as: | ||
|
||
// Acquire a context from the pool | ||
ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); | ||
|
||
// Instead of wrapping pipeline execution with try-catch, use ExecuteOutcomeAsync(...). | ||
// Certain strategies are optimized for this method, returning an exception instance without actually throwing it. | ||
Outcome<Member> outcome = await pipeline.ExecuteOutcomeAsync( | ||
static async (context, state) => | ||
{ | ||
// The callback for ExecuteOutcomeAsync must return an Outcome<T> instance. Hence, some wrapping is needed. | ||
try | ||
{ | ||
return Outcome.FromResult(await GetMemberAsync(state, context.CancellationToken)); | ||
} | ||
catch (Exception e) | ||
{ | ||
return Outcome.FromException<Member>(e); | ||
} | ||
}, | ||
context, | ||
id); | ||
|
||
// Handle exceptions using the Outcome<T> instance instead of try-catch. | ||
if (outcome.Exception is not null) | ||
{ | ||
logger.LogWarning(outcome.Exception, "Failed to get member with id '{id}'.", id); | ||
} | ||
|
||
// Release the context back to the pool | ||
ResilienceContextPool.Shared.Return(context); | ||
|
||
#endregion | ||
} | ||
|
||
private static ValueTask<Member> GetMemberAsync(string id, CancellationToken token) => default; | ||
|
||
public class Member | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters