Skip to content

Codegen endpoint operationContextParams JMESPath expressions - #7383

Merged
davidh44 merged 6 commits into
masterfrom
hdavidh/operationContextParams-jmesPath-lowering
Sep 22, 2026
Merged

davidh44 merged 6 commits into
masterfrom
hdavidh/operationContextParams-jmesPath-lowering

Conversation

@davidh44

@davidh44 davidh44 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Endpoint operationContextParams are currently bound by evaluating JMESPath expressions through the reflective JmesPathRuntime on every request. It wraps request values, scans SdkPojo.sdkFields() for each field access, and copies collections and maps, so cost grows with batch size.

These expressions are known at build time. This change generates direct typed getter calls for the supported patterns, removing reflection and most per-request allocation from the binding path.

Modifications

Generated <Service>EndpointResolverUtils.setOperationContextParams(...) methods now bind supported expressions with plain getters and loops instead of the reflective runtime. For S3 DeleteObjects (Delete.Objects[*].Key):

Before:

JmesPathRuntime.Value input = new JmesPathRuntime.Value(request);
params.deleteObjectKeys(input.field("Delete").field("Objects").wildcard().field("Key").stringValues());

After:

List<String> deleteObjectKeys = Collections.emptyList();
Delete delete = request.delete();
if (delete != null) {
    List<ObjectIdentifier> objects = delete.objects();
    deleteObjectKeys = new ArrayList<>(objects.size());
    for (ObjectIdentifier objectIdentifier : objects) {
        if (objectIdentifier != null) {
            String key = objectIdentifier.key();
            if (key != null) {
                deleteObjectKeys.add(key);
            }
        }
    }
}
params.deleteObjectKeys(deleteObjectKeys);

How it works:

  • A new package-private lowerer, JmesPathTypedGetterGenerator, converts a validated subset of expressions into typed Java: scalar string or boolean field chains, keys() over string-keyed maps, list projections ending in a string field, and a projected multiselect of string paths followed by one flatten.
  • Every path is resolved against the service model before any code is emitted. Unknown members, type mismatches, and members with runtime defaults (such as idempotency tokens) are rejected.
  • Rejection is signaled with a dedicated exception type, and if any binding for an operation is unsupported, the whole operation keeps the existing reflective method, so each generated method has a single evaluation strategy. An unexpected codegen error fails the build instead of silently falling back.
  • All bindings for an operation share one method-scoped NameAllocator, so generated locals cannot collide with the method parameters or with locals from earlier bindings.
  • Lowered output preserves null filtering, projection traversal order, and empty-list mutability. JMESPath defines keys() results as unordered, so generated code iterates the SDK request map directly instead of reproducing the reflective runtime's intermediate HashMap order.

This preserves defined JMESPath behavior and changes no public APIs. For keys(), the concrete first entry and initially selected account endpoint can change from the reflective runtime's hash order to request-map iteration. For the current DynamoDB bindings, mixed-account requests are ultimately handled through the general endpoint regardless of which account ID is first, so request behavior converges to the same result. Expressions outside the subset keep the exact code emitted today.

Scope

Six operations across DynamoDB and S3 use operationContextParams today. All six expressions fall within the supported subset, so each one generates typed getters and none uses the reflective fallback:

DynamoDB

  • BatchGetItem and BatchWriteItem: keys(RequestItems)
  • ImportTable: TableCreationParameters.TableName
  • TransactGetItems: TransactItems[*].Get.TableName
  • TransactWriteItems: TransactItems[*].[ConditionCheck.TableName, Put.TableName, Delete.TableName, Update.TableName][]

S3

  • DeleteObjects: Delete.Objects[*].Key

Testing

Added codegen unit tests covering accepted and rejected expression shapes, runtime-default rejection, name collisions, and whole-operation fallback. New S3 and DynamoDB tests feed the same request to both the lowered binding and the reflective runtime (which still ships as the fallback), comparing projections positionally and keys() results as unordered content while covering null containers, null elements, null leaves, and empty-list mutability. A third equivalence test against the synthetic stringarray service in test/codegen-generated-classes-test covers variants the real services don't reach, such as keys() behind a nullable struct and single-field multiselect branches. Each equivalence test also asserts the generated resolver's class file contains no JmesPathRuntime reference, so a regression that sends an operation back to the reflective path fails the test instead of comparing the fallback with itself.

Benchmark

Benchmarked endpoint resolution for S3 DeleteObjects and DynamoDB TransactWriteItems at the request sizes shown below. The benchmark measures endpoint parameter construction plus endpoint resolution, not end-to-end API latency. JMH ran one thread, five 2-second warmups, ten 5-second measurements, and three forks, with allocation profiling enabled. BatchGetItem was measured before the final review removed an unnecessary HashMap copy from its binding path; those stale rows are omitted rather than mixed with results for the final code.

Both sides of the A/B are current master (which includes the recent BDD endpoint provider adoption for S3 and DynamoDB) and differ only by this change, so the ns-saved and allocation columns isolate this change's effect; ratios express the improvement of the combined operation on the current engine.

S3 DeleteObjects:

keys base ns/op new ns/op ns saved ratio alloc B/op, base → new
1 1227.4 ± 6.2 220.4 ± 3.1 1007.0 5.6x 2816 → 328
10 3582.9 ± 6.4 273.0 ± 3.1 3309.9 13.1x 7896 → 360
100 27491.2 ± 20.2 781.4 ± 4.3 26709.8 35.2x 61593 → 720
1000 264299.5 ± 112.8 6183.8 ± 9.1 258115.7 42.7x 600047 → 4320

DynamoDB TransactWriteItems:

items base ns/op new ns/op ns saved ratio alloc B/op, base → new
1 1942.3 ± 6.1 193.9 ± 2.8 1748.4 10.0x 4667 → 304
10 13422.4 ± 17.8 260.5 ± 1.6 13161.9 51.5x 31606 → 336
100 133379.1 ± 55.9 892.6 ± 2.6 132486.5 149.4x 306387 → 696

All seven reported cases were faster and allocated less memory, with no overlapping latency error ranges.

The unaffected S3 GetObject control stayed stable: 199.4 ± 2.4 ns/op base, 197.3 ± 2.4 ns/op new. Its error ranges overlap, and allocation was identical (248 B/op).

@davidh44
davidh44 requested a review from a team as a code owner September 17, 2026 18:07
@davidh44 davidh44 added perf-improvement Label for PRs that contain performance improvement changes. no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required labels Sep 17, 2026
@alextwoods alextwoods changed the title Lower endpoint operationContextParams JMESPath expressions Codegen endpoint operationContextParams JMESPath expressions Sep 17, 2026

@alextwoods alextwoods left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stringarray service I think was orig mean to cover all of the supported operationContextParams (and string array cases) from when we added that - I think we have it in test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json - I think its worth adding tests (like what you did in ddb/s3) to verify the generated behavior

@davidh44

Copy link
Copy Markdown
Contributor Author

The stringarray service I think was orig mean to cover all of the supported operationContextParams (and string array cases) from when we added that - I think we have it in test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json - I think its worth adding tests (like what you did in ddb/s3) to verify the generated behavior

Good call. StringArrayBindingsTest in that module already covers the generated behavior end-to-end and passes against the lowered code (all three bindings lower, including the multiselect-list). Added an OperationContextParamsBindingEquivalenceTest there matching the DDB/S3 ones, comparing each lowered binding against the reflective evaluation, plus a keys()-behind-a-nullable-struct case that no real service exercises. All three equivalence tests now also assert the generated resolver has no JmesPathRuntime reference, so a regression back to the reflective path can't pass silently.

Comment thread .changes/next-release/feature-AWSSDKforJavav2-0f38bd9.json Outdated
@davidh44
davidh44 added this pull request to the merge queue Sep 22, 2026
Merged via the queue into master with commit 875805f Sep 22, 2026
13 of 14 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 Sep 22, 2026
@davidh44
davidh44 deleted the hdavidh/operationContextParams-jmesPath-lowering branch September 22, 2026 22:14
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required perf-improvement Label for PRs that contain performance improvement changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants