Skip to content

[Endpoints BDD 4/5] Avoid eager map allocation when building an Endpoint - #7317

Merged
alextwoods merged 1 commit into
feature/master/endpoints-bddfrom
alexwoo/endpoints-bdd-pr4
Aug 27, 2026
Merged

[Endpoints BDD 4/5] Avoid eager map allocation when building an Endpoint#7317
alextwoods merged 1 commit into
feature/master/endpoints-bddfrom
alexwoo/endpoints-bdd-pr4

Conversation

@alextwoods

@alextwoods alextwoods commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

[Endpoints BDD 4/5] Avoid eager map allocation when building an Endpoint

This PR is 4/5 for the Endpoints BDD:

Note: This PR merges to the feature/master/endpoints-bdd feature branch and NOT to master.

Motivation

Endpoint's builder eagerly allocated two HashMaps for headers and attributes for every endpoint, and passed them to the built Endpoint. Both escape into the result, so the JIT cannot remove them. Most endpoints need neither: headers are almost always absent, and attributes are almost always empty or a single AUTH_SCHEMES entry.

The builder itself does not escape resolveEndpoint, so escape analysis already eliminates it. This PR removes the allocations that actually survive.

No API change — no new methods, no signature changes.

Modifications

  • The header map is allocated only once a header is added.
  • The first attribute is staged in two fields on the builder; a map is allocated only if a second distinct key arrives.
  • build() collapses whatever was staged into the smallest immutable map that fits: emptyMap, singletonMap, or an unmodifiable view.
  • Both maps use initial capacity 4 instead of the default 16 (4 is slightly larger than the largest current known cases).

Benchmarks

JMH, allocation profiler, 2 forks, JDK 21, M-series. "Floor" is an Endpoint-shaped holder built only from emptyMap/singletonMap — what a hand-written static factory would allocate.

endpoint shape before after time
no attributes 120 B/op 24 B/op −67%
one attribute 232 B/op 64 B/op −71%
two attributes 264 B/op 232 B/op -22%
one attr + header 424 B/op 288 B/op −29%

Testing

  • endpoints-spi: 121 tests. 5 new covering 0/1/2/3-attribute arities, same-key overwrite on both the staged and promoted paths, toBuilder() round-trips across all arities, and headers() immutability for both empty and populated maps.

@alextwoods alextwoods changed the title perf(endpoints): Avoid eager map allocation when building an Endpoint [Endpoints BDD 4/4] Avoid eager map allocation when building an Endpoint Aug 25, 2026
@alextwoods
alextwoods force-pushed the alexwoo/endpoints-bdd-pr4 branch from 4a76bcc to 13c4e1d Compare August 26, 2026 03:44
@alextwoods alextwoods changed the title [Endpoints BDD 4/4] Avoid eager map allocation when building an Endpoint [Endpoints BDD 4/5] Avoid eager map allocation when building an Endpoint Aug 26, 2026
@alextwoods
alextwoods force-pushed the alexwoo/endpoints-bdd-pr4 branch from 13c4e1d to 8f6d3cf Compare August 26, 2026 21:16
@alextwoods
alextwoods marked this pull request as ready for review August 26, 2026 21:26
@alextwoods
alextwoods requested a review from a team as a code owner August 26, 2026 21:26
@alextwoods
alextwoods requested review from davidh44 and removed request for a team August 26, 2026 21:29
Base automatically changed from alexwoo/endpoints-bdd-pr3 to feature/master/endpoints-bdd August 26, 2026 21:49
Endpoint's builder eagerly allocated two HashMaps (headers and
attributes) for every endpoint, and handed them to the built Endpoint.
Both escape into the result, so they cannot be optimized away by the
JIT, and most endpoints need neither: headers are almost always absent
and attributes are almost always a single AUTH_SCHEMES entry.

Stage instead of allocate:

- The header map is allocated only once a header is actually added.
- The first attribute is held in two fields on the builder; a map is
  allocated only if a second distinct key arrives.
- build() collapses whatever was staged into the smallest immutable
  map that fits: emptyMap, singletonMap, or an unmodifiable view.
- Both maps are created with capacity 4 rather than the default 16,
  whose backing table was the largest single allocation on the path.

No API change. The builder itself does not escape resolveEndpoint, so
escape analysis already eliminates it; this removes the allocations
that actually survive.

Measured with JMH (allocation profiler, 2 forks, JDK 21, M-series),
against an Endpoint-shaped holder built only from emptyMap/singletonMap
as the theoretical floor:

  shape               before    after     floor
  no attributes       120 B/op   24 B/op   24 B/op
  one attribute       232 B/op   64 B/op   64 B/op
  two attributes      264 B/op  232 B/op
  one attr + header   424 B/op  288 B/op

The two shapes that dominate real endpoint rules now allocate exactly
what a hand-written factory would. Time per construction drops 67% and
71% for those two shapes. Two-attribute construction costs ~3ns more
because it stages an attribute and then promotes anyway; that path is
S3 Express only and 3ns against a ~50us endpoint resolution.

Behavior notes:

- headers() and the attribute map are now unmodifiable. Mutating a map
  returned from a getter was never supported, and SdkHttpRequest
  already behaves this way.
- Mutating a builder after build() no longer leaks into the Endpoint
  that was already built, for the no-attribute, single-attribute and
  header cases. Previously it leaked in every case. The two-or-more
  attribute case still shares state, unchanged from before.
@alextwoods
alextwoods force-pushed the alexwoo/endpoints-bdd-pr4 branch from 8f6d3cf to 17934a6 Compare August 26, 2026 21:49
@alextwoods
alextwoods merged commit 91541fd into feature/master/endpoints-bdd Aug 27, 2026
4 of 5 checks passed
@github-actions

Copy link
Copy Markdown

This pull request has been closed and the conversation has been locked. Comments on closed PRs are hard for our team to see. If you need more assistance, please open a new issue that references this one.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants