Skip to content

Commit

Permalink
[Docs] Introduce Performance article (#1600)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Sep 20, 2023
1 parent 7cc2ea1 commit 79fa6be
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 46 deletions.
21 changes: 6 additions & 15 deletions README_V8.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,13 @@ You can create a `ResiliencePipeline` using the `ResiliencePipelineBuilder` clas
<!-- snippet: quick-start -->
```cs
// Create a instance of builder that exposes various extensions for adding resilience strategies
var builder = new ResiliencePipelineBuilder();
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions()) // Add retry using the default options
.AddTimeout(TimeSpan.FromSeconds(10)) // Add 10 second timeout
.Build(); // Builds the resilience pipeline
// Add retry using the default options
builder.AddRetry(new RetryStrategyOptions());

// Add 10 second timeout
builder.AddTimeout(TimeSpan.FromSeconds(10));

// Build the resilience pipeline
ResiliencePipeline pipeline = builder.Build();

// Execute the pipeline
await pipeline.ExecuteAsync(async token =>
{
// Your custom logic here
});
// Execute the pipeline asynchronously
await pipeline.ExecuteAsync(async cancellationToken => { /*Your custom logic here */ }, cancellationToken);
```
<!-- endSnippet -->

Expand Down
130 changes: 130 additions & 0 deletions docs/advanced/performance.md
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 -->
21 changes: 6 additions & 15 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,13 @@ You can create a `ResiliencePipeline` using the `ResiliencePipelineBuilder` clas
<!-- snippet: quick-start -->
```cs
// Create a instance of builder that exposes various extensions for adding resilience strategies
var builder = new ResiliencePipelineBuilder();
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions()) // Add retry using the default options
.AddTimeout(TimeSpan.FromSeconds(10)) // Add 10 second timeout
.Build(); // Builds the resilience pipeline
// Add retry using the default options
builder.AddRetry(new RetryStrategyOptions());

// Add 10 second timeout
builder.AddTimeout(TimeSpan.FromSeconds(10));

// Build the resilience pipeline
ResiliencePipeline pipeline = builder.Build();

// Execute the pipeline
await pipeline.ExecuteAsync(async token =>
{
// Your custom logic here
});
// Execute the pipeline asynchronously
await pipeline.ExecuteAsync(async cancellationToken => { /*Your custom logic here */ }, cancellationToken);
```
<!-- endSnippet -->

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Polly has a rich documentation that covers various topics, such as:
- [Telemetry and monitoring](advanced/telemetry.md): How to access and analyze the data generated by Polly strategies and pipelines.
- [Dependency injection](advanced/dependency-injection.md): How to integrate Polly with dependency injection frameworks and containers.
- [Extensibility](advanced/extensibility.md): How to create and use custom strategies and extensions for Polly.
- [Performance](advanced/performance.md): Tips on optimizing and getting the best performance from Polly.
- [Chaos engineering](advanced/simmy.md): How to use Polly to inject faults and test the resilience of your system.

You can also find many resources and community contributions, such as:
Expand Down
2 changes: 2 additions & 0 deletions docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
href: advanced/dependency-injection.md
- name: Resilience context
href: advanced/resilience-context.md
- name: Performance
href: advanced/performance.md
- name: Chaos engineering
href: advanced/simmy.md

Expand Down
125 changes: 125 additions & 0 deletions src/Snippets/Docs/Performance.cs
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
{
}
}
23 changes: 8 additions & 15 deletions src/Snippets/Docs/Readme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,18 @@ internal static class Readme
{
public static async Task QuickStart()
{
CancellationToken cancellationToken = default;

#region quick-start

// Create a instance of builder that exposes various extensions for adding resilience strategies
var builder = new ResiliencePipelineBuilder();

// Add retry using the default options
builder.AddRetry(new RetryStrategyOptions());

// Add 10 second timeout
builder.AddTimeout(TimeSpan.FromSeconds(10));
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions()) // Add retry using the default options
.AddTimeout(TimeSpan.FromSeconds(10)) // Add 10 second timeout
.Build(); // Builds the resilience pipeline

// Build the resilience pipeline
ResiliencePipeline pipeline = builder.Build();

// Execute the pipeline
await pipeline.ExecuteAsync(async token =>
{
// Your custom logic here
});
// Execute the pipeline asynchronously
await pipeline.ExecuteAsync(async cancellationToken => { /*Your custom logic here */ }, cancellationToken);

#endregion
}
Expand Down
2 changes: 1 addition & 1 deletion src/Snippets/Snippets.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<ProjectType>Library</ProjectType>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064;S3257;IDE0028</NoWarn>
<NoWarn>$(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064;S3257;IDE0028;CA1031;CA1848</NoWarn>
<RootNamespace>Snippets</RootNamespace>
</PropertyGroup>

Expand Down

0 comments on commit 79fa6be

Please sign in to comment.