Skip to content

Commit ec8d501

Browse files
committed
Adding sync methods to IResiliencePolicy
1 parent 23e5096 commit ec8d501

9 files changed

Lines changed: 876 additions & 157 deletions

File tree

benchmarks/RESILIENCE_BENCHMARK_RESULTS.md

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,96 @@ This benchmark compares the performance overhead of Foundatio's `IResiliencePoli
44

55
## Results Summary
66

7-
| Method | Category | Mean | Ratio | Allocated |
8-
|--------|----------|------|-------|-----------|
9-
| Direct_NoRetry | 1_NoRetry | 2.84 ns | 1.00 | - |
10-
| Foundatio_NoRetry | 1_NoRetry | 33.59 ns | 11.84x | 64 B |
11-
| Polly_NoRetry | 1_NoRetry | 32.57 ns | 11.48x | 64 B |
12-
| | | | | |
13-
| Direct_WithRetry | 2_WithRetry | 2.84 ns | 1.00 | - |
14-
| Foundatio_WithRetry | 2_WithRetry | 33.62 ns | 11.84x | 64 B |
15-
| Polly_WithRetry | 2_WithRetry | 137.44 ns | 48.41x | 64 B |
16-
| | | | | |
17-
| Direct_NoRetry_WithResult | 3_NoRetry_WithResult | 3.09 ns | 1.00 | - |
18-
| Foundatio_NoRetry_WithResult | 3_NoRetry_WithResult | 35.46 ns | 11.47x | 64 B |
19-
| Polly_NoRetry_WithResult | 3_NoRetry_WithResult | 32.79 ns | 10.61x | 64 B |
20-
| | | | | |
21-
| Direct_WithRetry_WithResult | 4_WithRetry_WithResult | 3.10 ns | 1.00 | - |
22-
| Foundatio_WithRetry_WithResult | 4_WithRetry_WithResult | 36.34 ns | 11.72x | 64 B |
23-
| Polly_WithRetry_WithResult | 4_WithRetry_WithResult | 104.98 ns | 33.87x | 64 B |
7+
### Synchronous Execution
8+
9+
| Method | Category | Mean | Allocated |
10+
| -------------------------- | ------------------ | --------- | --------- |
11+
| Direct_Sync_NoRetry | 1_Sync_NoRetry | 0.28 ns | - |
12+
| Foundatio_Sync_NoRetry | 1_Sync_NoRetry | 23.29 ns | - |
13+
| Polly_Sync_NoRetry | 1_Sync_NoRetry | 19.92 ns | - |
14+
| | | | |
15+
| Direct_Sync_WithRetry | 2_Sync_WithRetry | 0.18 ns | - |
16+
| Foundatio_Sync_WithRetry | 2_Sync_WithRetry | 23.01 ns | - |
17+
| Polly_Sync_WithRetry | 2_Sync_WithRetry | 121.87 ns | - |
18+
| | | | |
19+
| Direct_Sync_WithResult | 3_Sync_WithResult | 0.26 ns | - |
20+
| Foundatio_Sync_WithResult | 3_Sync_WithResult | 25.11 ns | - |
21+
| Polly_Sync_WithResult | 3_Sync_WithResult | 126.83 ns | 24 B |
22+
23+
### Asynchronous Execution
24+
25+
| Method | Category | Mean | Allocated |
26+
| -------------------------- | ------------------ | --------- | --------- |
27+
| Direct_Async_NoRetry | 4_Async_NoRetry | 2.80 ns | - |
28+
| Foundatio_Async_NoRetry | 4_Async_NoRetry | 36.39 ns | 64 B |
29+
| Polly_Async_NoRetry | 4_Async_NoRetry | 36.61 ns | 64 B |
30+
| | | | |
31+
| Direct_Async_WithRetry | 5_Async_WithRetry | 2.83 ns | - |
32+
| Foundatio_Async_WithRetry | 5_Async_WithRetry | 36.86 ns | 64 B |
33+
| Polly_Async_WithRetry | 5_Async_WithRetry | 141.24 ns | 64 B |
34+
| | | | |
35+
| Direct_Async_WithResult | 6_Async_WithResult | 3.11 ns | - |
36+
| Foundatio_Async_WithResult | 6_Async_WithResult | 38.65 ns | 64 B |
37+
| Polly_Async_WithResult | 6_Async_WithResult | 115.35 ns | 64 B |
38+
39+
### Zero-Allocation Patterns
40+
41+
| Method | Category | Mean | Allocated |
42+
| -------------------------- | ------------------ | --------- | --------- |
43+
| Direct_ZeroAlloc_Static | 7_ZeroAlloc_Static | 1.16 ns | - |
44+
| Foundatio_ZeroAlloc_Static | 7_ZeroAlloc_Static | 29.21 ns | - |
45+
| Polly_ZeroAlloc_Static | 7_ZeroAlloc_Static | 133.70 ns | - |
46+
| | | | |
47+
| Direct_ZeroAlloc_State | 8_ZeroAlloc_State | 1.04 ns | - |
48+
| Foundatio_ZeroAlloc_State | 8_ZeroAlloc_State | 30.71 ns | - |
49+
| Polly_ZeroAlloc_State | 8_ZeroAlloc_State | 130.71 ns | 88 B |
2450

2551
## Key Findings
2652

27-
### No Retry Configured (Base Framework Overhead)
53+
### Sync Performance
2854

29-
When no retry policy is configured, both libraries perform nearly identically:
55+
| Scenario | Foundatio | Polly | Foundatio Advantage |
56+
| --------------- | --------- | --------- | --------------------- |
57+
| No retry | 23.3 ns | 19.9 ns | Polly ~17% faster |
58+
| With retry | 23.0 ns | 121.9 ns | **5.3x faster** |
59+
| With result | 25.1 ns | 126.8 ns | **5.1x faster** |
3060

31-
| Scenario | Foundatio | Polly | Difference |
32-
|----------|-----------|-------|------------|
33-
| Async (void) | 33.6 ns | 32.6 ns | Polly ~3% faster |
34-
| With Result | 35.5 ns | 32.8 ns | Polly ~8% faster |
61+
Sync execution in Foundatio allocates **0 bytes** across all scenarios. Polly allocates 24 bytes when returning results.
3562

36-
**Conclusion**: For pass-through scenarios with no retry logic, Polly's empty pipeline has a slight edge.
63+
### Async Performance
3764

38-
### With Retry Configured (Real-World Scenario)
65+
| Scenario | Foundatio | Polly | Foundatio Advantage |
66+
| --------------- | --------- | --------- | --------------------- |
67+
| No retry | 36.4 ns | 36.6 ns | ~equal |
68+
| With retry | 36.9 ns | 141.2 ns | **3.8x faster** |
69+
| With result | 38.7 ns | 115.3 ns | **3.0x faster** |
3970

40-
When retry policies are configured (3 attempts, even though no retries occur), Foundatio is significantly faster:
71+
Both libraries allocate 64 bytes for async execution (async state machine overhead).
4172

42-
| Scenario | Foundatio | Polly | Foundatio Advantage |
43-
|----------|-----------|-------|---------------------|
44-
| Async (void) | 33.6 ns | 137.4 ns | **4.1x faster** |
45-
| With Result | 36.3 ns | 105.0 ns | **2.9x faster** |
73+
### Zero-Allocation Performance
4674

47-
**Conclusion**: Foundatio maintains consistent ~34-36ns overhead regardless of configuration, while Polly's overhead increases 3-4x when retry strategies are added.
75+
For performance-critical paths, Foundatio's state-based overloads achieve **zero heap allocations**:
4876

49-
### Memory Allocations
77+
| Pattern | Foundatio | Polly | Foundatio Advantage |
78+
| -------------------- | ---------------- | --------------- | --------------------------------- |
79+
| Static lambda | 29.2 ns, **0 B** | 133.7 ns, 0 B | **4.6x faster** |
80+
| State-based | 30.7 ns, **0 B** | 130.7 ns, 88 B | **4.3x faster, zero allocations** |
5081

51-
Both libraries allocate **64 bytes** per operation when using resilience policies. The direct call allocates nothing.
82+
Polly still allocates 88 bytes even with the state-based pattern, while Foundatio achieves true zero-allocation execution.
5283

53-
### Why Foundatio is Faster with Retry Configuration
84+
## Why Foundatio is Faster
5485

5586
**Foundatio's architecture:**
87+
5688
- Simple `do-while` loop with direct property checks
5789
- Null-conditional operators for optional features (`CircuitBreaker?.BeforeCall()`)
5890
- Direct field access with no indirection layers
5991
- When `Timeout <= 0`, no `CancellationTokenSource` allocations
60-
- On success path: calls action, checks circuit breaker (null), returns
92+
- Dedicated sync `Execute` methods (no async overhead)
93+
- State-based overloads avoid closure allocations entirely
6194

6295
**Polly's architecture:**
96+
6397
- Pipeline-based with strategy composition
6498
- Each strategy (retry, circuit breaker, timeout) is a separate component in a chain
6599
- `ResiliencePipeline.ExecuteAsync` iterates through the strategy stack
@@ -73,7 +107,7 @@ The flexibility of Polly's composable pipeline architecture comes at a performan
73107
### Foundatio Setup
74108

75109
```csharp
76-
// No Retry: 1 attempt, no delay (measures base framework overhead)
110+
// No Retry: 1 attempt, no delay
77111
_foundatioNoRetry = new ResiliencePolicyBuilder()
78112
.WithMaxAttempts(1)
79113
.WithDelay(TimeSpan.Zero)
@@ -89,9 +123,8 @@ _foundatioWithRetry = new ResiliencePolicyBuilder()
89123
### Polly Setup
90124

91125
```csharp
92-
// No Retry: Empty pipeline (no retry strategy)
93-
_pollyNoRetry = new ResiliencePipelineBuilder()
94-
.Build();
126+
// No Retry: Empty pipeline
127+
_pollyNoRetry = new ResiliencePipelineBuilder().Build();
95128

96129
// With Retry: 2 retries (3 total attempts)
97130
_pollyWithRetry = new ResiliencePipelineBuilder()
@@ -103,29 +136,30 @@ _pollyWithRetry = new ResiliencePipelineBuilder()
103136
.Build();
104137
```
105138

106-
### Simulated Operations
139+
### Zero-Allocation Examples
107140

108141
```csharp
109-
// Minimal work to measure framework overhead, not operation cost
110-
private ValueTask SimulateWorkAsync()
142+
// Foundatio state-based (0 B allocations)
143+
_foundatioWithRetry.ExecuteAsync(this, static (state, ct) =>
111144
{
112-
Interlocked.Increment(ref _counter);
113-
return ValueTask.CompletedTask;
114-
}
145+
return state.SimulateWorkAsync();
146+
});
115147

116-
private ValueTask<int> SimulateWorkWithResultAsync()
117-
{
118-
return ValueTask.FromResult(Interlocked.Increment(ref _counter));
119-
}
148+
// Polly state-based (88 B allocations)
149+
_pollyWithRetry.ExecuteAsync(
150+
static (state, ct) => state.SimulateWorkAsync(),
151+
this);
120152
```
121153

122154
## Recommendations
123155

124156
### When to use Foundatio ResiliencePolicy
125157

126-
1. **Production workloads with retry policies**: 3-4x faster than Polly when retries are configured
127-
2. **Consistent performance requirements**: Overhead is predictable (~34-36ns) regardless of configuration
128-
3. **High-frequency operations**: Lower overhead matters when called millions of times
158+
1. **Production workloads with retry policies**: 3-5x faster than Polly when retries are configured
159+
2. **Zero-allocation requirements**: State-based overloads achieve true zero-allocation execution
160+
3. **Sync execution**: Native sync methods without async overhead
161+
4. **Consistent performance**: Overhead is predictable (~23-39ns) regardless of configuration
162+
5. **High-frequency operations**: Lower overhead matters when called millions of times
129163

130164
### When to use Polly
131165

0 commit comments

Comments
 (0)