Skip to content

Trim per-request work in the HttpClient adapter - #4561

Open
jnunn-aws wants to merge 4 commits into
developmentfrom
jonatnun/http-adapter-trims
Open

jnunn-aws wants to merge 4 commits into
developmentfrom
jonatnun/http-adapter-trims

Conversation

@jnunn-aws

@jnunn-aws jnunn-aws commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • HttpClientResponseData: copy each response header once from the
    enumerated values instead of re-looking every key up with TryGetValues;
    build GetHeaderNames lazily from the dictionary and answer
    IsHeaderPresent from the dictionary instead of a second HashSet.
  • User-Agent: reuse one of the last four generated strings per HttpHandler when the
    ordered components and feature ids match exactly (a client alternating
    sync and async calls, or operations with different feature ids, keeps one
    entry per shape); merge request-level
    UA details without the intermediate join/enumerator; avoid the UTF-8
    byte[] in the 1 KB truncation check.
  • HttpHandler: reuse the previously parsed request Uri when the composed
    URL string (and the canonicalization setting) is unchanged (non-BCL);
    skip attribute building and boxing in RecordHttpTelemetryData when the
    trace span and meter provider are the SDK's no-op ones; skip RequestSize boxing when RequestMetrics
    is disabled.
  • HttpWebRequestMessage: use the shared HttpMethod instances for the
    standard methods; enumerate concrete Dictionary headers without boxing.

HttpAdapterTests expects the runtime's own exception type for
HttpMethod(null): netcoreapp3.1 throws ArgumentException where net472 and
.NET 5+ throw ArgumentNullException, and GetHttpMethod passes null through
unchanged.

Motivation and Context

x86 ARM64
ops/CPU-sec, this change alone +4.3% +6.8%

Measurement: Numbers come from the serde ops/CPU-sec runner (sdk/test/Performance/SerdeBenchmarks, --cpu-time, stub transport), geomean of 71 cases, 3 interleaved runs. Instances: m7i.metal-24xl (x86) and m7g.metal (ARM64). Settings: net8.0, 20,000-iteration warmup, measured on development 8956121, which has the same code as this branch's base for these files. Each change is measured alone against that base. Both metal runs measured an earlier revision of this PR that kept one User-Agent string per client instead of four. On feature/4.1 (337f357, net10.0, 2,000-iteration warmup, 3 interleaved rounds, same instances) this change gained +5.8% on x86 and +5.1% on ARM. The final run at the harness defaults (net10.0, 100,000 warmup, stub and https) is tracked separately.

Clients that alternate request shapes. A cache miss costs no more than today's code. In a development-host run (one client, alternating GetObject keys and/or User-Agent components, stub transport, 9 interleaved reps), every alternating pattern was 6.6–7.8% cheaper per call than development, against 7.4% when nothing alternates. The −3.4% on awsQuery_GetMetricDataResponse_S in the development run didn't reproduce on feature/4.1 (+4.9% x86, +3.9% ARM), where the worst of the 71 cases was −0.9% on x86, within the noise.

Testing

  • New HttpAdapterTests (29 on net8.0): response headers match a reference copy; User-Agent reuse regenerates when inputs differ and matches the generated header across requests; ComposeRequestUrl matches ComposeUrl for paths, queries and sub-resources and honors canonicalization changes, using the canonicalization setting read for the cache key; RequestSize recorded only when request metrics are enabled (sync, and the async path on net472); standard HttpMethod reuse; HTTP metrics and span attributes with custom and no-op providers; a case-only name collision between response and content headers keeps the response header; User-Agent reuse with alternating shapes and with more shapes than cache entries.
  • Core unit tests pass on netcoreapp3.1 and net8.0 (development), and on net8.0 and net10.0 with this change applied to feature/4.1 7d795cb.

Breaking Changes Assessment

None to the public API. HttpClientResponseData (Amazon.Runtime.Internal.Transform) no longer throws when a response header and a content header differ only in case (the response header's value is kept), and IsHeaderPresent returns false instead of throwing NullReferenceException on event-stream message data.

Credit

@dscpinheiro independently arrived at the same response-side change in 9b07f37: copying each response header once and answering IsHeaderPresent/GetHeaderNames from the header dictionary.

Dry-runs

  • DotNet Dry-run ID: 71ac0493-f653-41b9-83d1-de0212b1ff30
    • Completed successfully
  • PowerShell Dry-run ID: f9ba88a1-149a-4962-9861-e7055ae8d82c
    • Completed successfully

Checklist

  • My code follows the code style of this project
  • I have added tests to cover my changes
  • All new and existing tests passed

License

  • I confirm that this pull request can be released under the Apache 2 license

- HttpClientResponseData: copy each response header once from the
  enumerated values instead of re-looking every key up with TryGetValues;
  build GetHeaderNames lazily from the dictionary and answer
  IsHeaderPresent from the dictionary instead of a second HashSet.
- User-Agent: reuse the last generated string per HttpHandler when the
  ordered components and feature ids are unchanged; merge request-level
  UA details without the intermediate join/enumerator; avoid the UTF-8
  byte[] in the 1 KB truncation check.
- HttpHandler: reuse the previously parsed request Uri when the composed
  URL string (and the canonicalization setting) is unchanged (non-BCL);
  skip attribute building/boxing in RecordHttpTelemetryData when the
  meter/tracer providers are the built-in no-op ones; skip RequestSize
  boxing when RequestMetrics is disabled.
- HttpWebRequestMessage: use the shared HttpMethod instances for the
  standard methods; enumerate concrete Dictionary headers without boxing.

HttpAdapterTests expects the runtime's own exception type for
HttpMethod(null): netcoreapp3.1 throws ArgumentException where net472 and
.NET 5+ throw ArgumentNullException, and GetHttpMethod passes null through
unchanged.
@jnunn-aws
jnunn-aws marked this pull request as ready for review October 28, 2026 07:03
@jnunn-aws
jnunn-aws requested review from a team as code owners October 28, 2026 07:03
@jnunn-aws
jnunn-aws requested review from dscpinheiro and sichanyoo and removed request for a team October 28, 2026 07:03
Merge near-duplicate tests into data rows, assert literal User-Agent values,
and use the shared RecordingTelemetry.cs helpers. The response-header test
still compares against the previous copy algorithm.
Only tests passed a cached entry; HttpHandler.GetUserAgent already does the
matching through UserAgentDetails.Matches, so the method no longer takes one.
@dscpinheiro dscpinheiro added the perf Performance related improvements label Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf Performance related improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants