Skip to content

perf: reduce Refit.Reflection memory allocations#2267

Merged
glennawatson merged 2 commits into
mainfrom
perf/reflection-allocations
Jul 20, 2026
Merged

perf: reduce Refit.Reflection memory allocations#2267
glennawatson merged 2 commits into
mainfrom
perf/reflection-allocations

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Performance / refactor of the Refit.Reflection library (the reflection request builder). No behavioural change and no public API change.

What is the new behavior?

Reduced memory allocations across the reflection request builder's per-request and per-method-parse paths, guided by a new GcVerbose micro-benchmark suite and verified behaviour-identical. Memory was the primary goal (time improvements are a bonus).

  • Query subsystem: cache per-type query-property metadata and the attribute provider, cache the enumerable classification, drop an iterator, and parse an existing query string span-based without a NameValueCollection - object/collection query flatten down 69-82%.
  • Object-path / multipart / headers: cache per-parameter attributes, build the route object-property lookup lazily, hold route bindings in a value tuple, emit static headers without a per-request copy.
  • Cold parse: read each parameter's attributes in one GetCustomAttributes pass, cache the declared factory-method lookup (FindDeclaredMethod -> 0 B, BuildRestResultFuncForMethod -76%), lazy/presize the parameter maps.
  • Adds a Refit.Reflection.Benchmarks GcVerbose micro-benchmark project (91 benchmarks) and a Refit.Reflection.Tests project pinning the request-builder contracts; the relevant private members are widened to internal (fields stay private) so the benchmarks can reach them.

What is the current behavior?

The reflection request builder re-read attributes per call, materialised intermediate collections, and re-scanned method metadata, so each request build allocated more than necessary.

What might this PR break?

None. Behaviour and serialised output are unchanged, verified by Refit.Tests (1152) plus a new Refit.Reflection.Tests (41) pinning the request-builder contracts. No public API changes; the widened members are internal.

Additional information

Component-level allocation reductions (GcVerbose Allocated, deterministic) - the primary goal:

Path Before After
Query object flatten (BuildQueryMapObject) 4561 B 816 B (-82%)
Query object add (AddQueryParametersObject) 5401 B 1656 B (-69%)
FindDeclaredMethod 1696 B 0 B
BuildRestResultFuncForMethod 2232 B 536 B (-76%)
ConstructBuilder (parse all method infos) 77.7 KB 64.3 KB (-17%)
BuildParameterMapMultiSegment 4952 B 3768 B (-24%)

End-to-end request build (public reflection path, net8.0) - the high-level A/B:

Method main branch
Query object + path + header 6.54 us 5.16 us (-21% time)
JSON body 3.20 us 3.13 us (same time, lower allocation)

Every remaining allocation was trace-verified against its GcVerbose stack and is either the produced output (the request message, its Options/Uri/headers/body content, the built delegate, or stored value-equatable metadata), a framework allocation inside a call the reflection path must make (System.Reflection/System.Text.Json/System.Uri/HttpClient), or a value-type box the reflection API forces - nothing reducible remains in Refit.Reflection code.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (fb87be5) to head (95ad9dc).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #2267   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          183       187    +4     
  Lines         9745      9835   +90     
  Branches      1867      1895   +28     
=========================================
+ Hits          9745      9835   +90     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@glennawatson
glennawatson force-pushed the perf/reflection-allocations branch from 03cd0b4 to 7c0d90d Compare July 20, 2026 14:45
Cut allocations across the reflection request builder's per-request and
per-method-parse paths, behaviour-identical, guided by GcVerbose micro-benchmarks.

- Query subsystem: cache per-type query-property metadata and the attribute
  provider, cache the enumerable classification, drop the iterator, and parse an
  existing query string span-based without a NameValueCollection - object/collection
  query flatten down 69-82%.
- Object-path / multipart / headers: cache per-parameter attributes, build the
  route object-property lookup lazily, value-tuple bindings, emit static headers
  without a per-request copy.
- Cold parse: read each parameter's attributes in one GetCustomAttributes pass,
  cache the declared factory-method lookup (FindDeclaredMethod -> 0 B,
  BuildRestResultFuncForMethod -76%), lazy/presize the parameter maps.
- Add a Refit.Reflection.Benchmarks GcVerbose micro-benchmark project and a
  Refit.Reflection.Tests project pinning the request-builder contracts; widen the
  relevant private members to internal (fields stay private) for benchmarking.

End-to-end reflection request build is ~21% faster on a query-object method
(6.5 -> 5.2 us). No public API change; behaviour and serialised output unchanged.
@glennawatson
glennawatson force-pushed the perf/reflection-allocations branch from 7c0d90d to 495de36 Compare July 20, 2026 14:46
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit 8382bed into main Jul 20, 2026
17 checks passed
@glennawatson
glennawatson deleted the perf/reflection-allocations branch July 20, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants