Skip to content

Improve pipeline execution performance - #3287

Merged
martincostello merged 3 commits into
mainfrom
improve-performance
Oct 25, 2026
Merged

martincostello merged 3 commits into
mainfrom
improve-performance

Conversation

@martincostello

@martincostello martincostello commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Pull Request

The issue or feature being addressed

Improve the performance of the hot path when executing resilience pipelines.

Details on the issue fix or feature implementation

Profiling a pipeline of five strategies showed that a significant part of the time Polly itself spends per execution is generic virtual method (GVM) dispatch between the components of the pipeline chain (each hop costs roughly 7 ns and there were up to three per strategy), plus the circuit breaker taking its lock twice per execution.

  • Avoid GVM dispatch between delegating components in the pipeline chain. DelegatingComponent exposes a non-virtual Execute() method that the composite root and the previous link in the chain call directly when the next component is a DelegatingComponent.
  • Execute non-generic strategies directly rather than through their BridgeComponent, which is a trivial forwarder. DelegatingComponent unwraps the strategy at construction time.
  • Skip the circuit breaker lock on pre-execution when the circuit is closed. A closed circuit cannot transition to another state in OnActionPreExecuteAsync(), so a volatile read is sufficient and a concurrent transition after the read is indistinguishable from one that happens right after the lock would have been released.

There are no public API changes and no changes to allocations.

Benchmarks

Benchmarks were run against main (3ab3ec0) in a separate worktree and against this branch, pinned to the performance cores (--affinity 4095) with two interleaved rounds per side. The figures below are the mean of the two rounds, and Ratio is branch/main.

Benchmark Parameters main (ns) branch (ns) Ratio Allocated
PipelineBenchmark.ExecutePipeline_V8 Components=1 37.2 34.7 0.93 -
PipelineBenchmark.ExecutePipeline_V8 Components=2 50.8 46.8 0.92 -
PipelineBenchmark.ExecutePipeline_V8 Components=5 112.6 90.9 0.81 -
PipelineBenchmark.ExecutePipeline_V8 Components=10 236.4 200.3 0.85 -
MultipleStrategiesBenchmark.ExecuteStrategyPipeline_Generic_V8 619.6 569.7 0.92 40 B
MultipleStrategiesBenchmark.ExecuteStrategyPipeline_NonGeneric_V8 742.4 649.6 0.88 40 B
CircuitBreakerBenchmark.ExecuteCircuitBreaker_V8 130.3 115.8 0.89 -
TimeoutBenchmark.ExecuteTimeout_V8 126.2 121.0 0.96 -
RetryBenchmark.ExecuteRetry_V8 105.8 99.1 0.94 -

The retry strategy is generic and is not affected by these changes, so RetryBenchmark acts as a control for run-to-run noise on this machine (its two rounds were 110.0/101.5 ns on main and 99.1/99.2 ns on this branch). The 40 B allocated by MultipleStrategiesBenchmark is the ConcurrencyLease from System.Threading.RateLimiting and is unchanged.

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26300.9711)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.402
  [Host] : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v3
Job=MediumRun  Toolchain=InProcessEmitToolchain  IterationCount=15  LaunchCount=2  WarmupCount=10

An attempt to skip the retry strategy's GetTimestamp() calls when neither telemetry nor OnRetry can observe the duration was measured at ~1.7 ns and discarded as noise.

Confirm the following

  • I started this PR by branching from the head of the default branch
  • I have targeted the PR to merge into the default branch
  • I have included unit tests for the issue/feature
  • I have successfully run a local build
  • I have the experience and knowledge necessary to understand, review, and validate all content in this PR.
  • This PR has content that I did not fully write myself.
    • I used AI to assist in writing or reviewing the content of this PR.

Note

This PR and its description were generated with Claude Code.

- Avoid generic virtual method dispatch between delegating components in the pipeline chain.
- Execute non-generic strategies directly rather than through their bridge component.
- Skip the circuit breaker lock on pre-execution when the circuit is closed.
@martincostello martincostello added this to the v8.8.1 milestone Oct 25, 2026
@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.03%. Comparing base (3ab3ec0) to head (4dd7b9b).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3287   +/-   ##
=======================================
  Coverage   98.02%   98.03%           
=======================================
  Files         321      321           
  Lines        7103     7131   +28     
  Branches     1089     1099   +10     
=======================================
+ Hits         6963     6991   +28     
  Misses         98       98           
  Partials       42       42           
Flag Coverage Δ
linux 98.02% <100.00%> (+0.05%) ⬆️
macos 98.02% <100.00%> (+<0.01%) ⬆️
windows 98.03% <100.00%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Add more test coverage for `DelegatingComponent`.
Add test for missing coverage (hopefully).
@martincostello
martincostello marked this pull request as ready for review October 25, 2026 15:34
Copilot AI balanced review requested due to automatic review settings October 25, 2026 15:34

This comment was marked as resolved.

@martincostello
martincostello enabled auto-merge (squash) October 25, 2026 15:35
@martincostello
martincostello merged commit e762964 into main Oct 25, 2026
28 checks passed
@martincostello
martincostello deleted the improve-performance branch October 25, 2026 15:36
SonnyRR pushed a commit to SonnyRR/stacked-deck-templates that referenced this pull request Oct 26, 2026
Updated [Polly](https://github.com/App-vNext/Polly) from 8.8.0 to 8.8.1.

<details>
<summary>Release notes</summary>

_Sourced from [Polly's
releases](https://github.com/App-vNext/Polly/releases)._

## 8.8.1

## Highlights

* Prevent the circuit breaker from capturing the ambient
ExecutionContext by @​Nacl-HCl in
App-vNext/Polly#3284
* Improve pipeline execution performance by @​martincostello in
App-vNext/Polly#3287
* Fix cleanup of discarded registry pipelines by @​harshvarudkar in
App-vNext/Polly#3288

## What's Changed

* Add Copilot code review skill by @​martincostello in
App-vNext/Polly#3247
* Fix benchmarks by @​martincostello in
App-vNext/Polly#3248
* Update PR template by @​martincostello in
App-vNext/Polly#3249
* Update README with OSMF details for Polly by @​joelhulen in
App-vNext/Polly#3263
* Fix lint warnings by @​martincostello in
App-vNext/Polly#3264
* Update FUNDING by @​martincostello in
App-vNext/Polly#3258
* Remove obsolete packages references by @​martincostello in
App-vNext/Polly#3267
* Validate packages in parallel by @​martincostello in
App-vNext/Polly#3283
* .NET 11 preparation by @​martincostello in
App-vNext/Polly#3285

## New Contributors

* @​Nacl-HCl made their first contribution in
App-vNext/Polly#3284
* @​harshvarudkar made their first contribution in
App-vNext/Polly#3288

**Full Changelog**:
App-vNext/Polly@8.8.0...8.8.1

Commits viewable in [compare
view](App-vNext/Polly@8.8.0...8.8.1).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Polly&package-manager=nuget&previous-version=8.8.0&new-version=8.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants