From 8afdf96f362ea1998c3df0aa8f6f30f02c116fef Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 19 May 2026 16:34:41 -0500 Subject: [PATCH] Improve what we pre-compute for literal query We were precomputing arrays of un-encoded query string key value pairs for literal query string parts of a URI. This instead pre-computes the already encoded query literals. --- .../binding/HttpBindingSchemaExtensions.java | 43 ++++++++++++------- .../http/binding/HttpBindingSerializer.java | 11 ++--- .../java/io/uri/QueryStringBuilder.java | 17 ++++++++ 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSchemaExtensions.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSchemaExtensions.java index c40d3f1a8..3ffeeb033 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSchemaExtensions.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSchemaExtensions.java @@ -21,6 +21,7 @@ import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.java.core.serde.TimestampFormatter; import software.amazon.smithy.java.http.api.HeaderName; +import software.amazon.smithy.java.io.uri.URLEncoding; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.traits.HttpTrait; import software.amazon.smithy.utils.SmithyInternalApi; @@ -40,6 +41,10 @@ public final class HttpBindingSchemaExtensions */ public static final SchemaExtensionKey KEY = new SchemaExtensionKey<>(); + private static final Schema[] NO_SCHEMAS = new Schema[0]; + private static final HeaderName[] NO_HEADER_NAMES = new HeaderName[0]; + private static final String[] NO_STRINGS = new String[0]; + /** * Look up the {@link MemberBinding} for a member schema. Throws if no binding or not a member. */ @@ -87,9 +92,6 @@ static StructBindings structBindingsOf(Schema schema) { return sb; } - private static final Schema[] NO_SCHEMAS = new Schema[0]; - private static final String[] NO_QUERY_LITERALS = new String[0]; - /** * Binding kind for a single member, derived from the traits applied to it. * @@ -356,12 +358,14 @@ ByteBuffer emptyBody(Codec codec, Schema schema) { * Pre-computed HTTP-binding data for an operation schema. * * @param httpTrait the cached {@code @http} trait — saves an {@code expectTrait} call per request. - * @param queryLiterals flat array of (name, value) pairs from the URI's static query literals, or empty. + * @param queryLiteralKeys raw {@code @http} URI query literal keys (e.g. {@code ["x-id"]}). + * @param queryLiteralEntries pre-encoded {@code "key=value"} strings parallel to {@link #queryLiteralKeys}. * @param defaultResponseStatus default response status declared by the {@code @http} trait. */ record OperationBinding( HttpTrait httpTrait, - String[] queryLiterals, + String[] queryLiteralKeys, + String[] queryLiteralEntries, int defaultResponseStatus) implements HttpBindingExt {} @Override @@ -732,9 +736,6 @@ private static Schema[] toArray(List list) { return list.isEmpty() ? NO_SCHEMAS : list.toArray(new Schema[0]); } - private static final HeaderName[] NO_HEADER_NAMES = new HeaderName[0]; - private static final String[] NO_STRINGS = new String[0]; - /** * Pre-resolve canonical {@link HeaderName}s parallel to a {@code Schema[]} of * list-header members. Reading the name later in the deserializer becomes a @@ -772,21 +773,31 @@ private static OperationBinding forOperation(Schema schema) { var httpTrait = schema.expectTrait(TraitKey.HTTP_TRAIT); var uriPattern = httpTrait.getUri(); - // Flatten query literals into a (name, value) pair array. The trait's own map iteration is stable for a - // given trait instance, but this avoids the per-call iterator + Map.Entry allocations. + // Pre-encode static @http URI query literals into "key=value" strings so the request hot path can append + // them verbatim instead of re-encoding per call. var queryLiteralMap = uriPattern.getQueryLiterals(); - String[] queryLiterals; + String[] queryLiteralKeys; + String[] queryLiteralEntries; if (queryLiteralMap.isEmpty()) { - queryLiterals = NO_QUERY_LITERALS; + queryLiteralKeys = NO_STRINGS; + queryLiteralEntries = NO_STRINGS; } else { - queryLiterals = new String[2 * queryLiteralMap.size()]; + int n = queryLiteralMap.size(); + queryLiteralKeys = new String[n]; + queryLiteralEntries = new String[n]; int i = 0; + StringBuilder pair = new StringBuilder(); for (var entry : queryLiteralMap.entrySet()) { - queryLiterals[i++] = entry.getKey(); - queryLiterals[i++] = entry.getValue(); + pair.setLength(0); + URLEncoding.encodeUnreserved(entry.getKey(), pair, false); + pair.append('='); + URLEncoding.encodeUnreserved(entry.getValue(), pair, false); + queryLiteralKeys[i] = entry.getKey(); + queryLiteralEntries[i] = pair.toString(); + i++; } } - return new OperationBinding(httpTrait, queryLiterals, httpTrait.getCode()); + return new OperationBinding(httpTrait, queryLiteralKeys, queryLiteralEntries, httpTrait.getCode()); } } diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java index 14f5d3561..72241b7cb 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java @@ -144,12 +144,13 @@ public void writeStruct(Schema schema, SerializableStruct struct) { headers = HttpHeaders.ofModifiable(headerCount); - // Add fixed query string parameters from @http trait's uri field. - String[] qLits = operationBinding.queryLiterals(); - if (qLits.length > 0) { + // Append the static @http URI query literals + String[] qKeys = operationBinding.queryLiteralKeys(); + if (qKeys.length > 0) { QueryStringBuilder qsb = queryStringParams(); - for (int i = 0; i < qLits.length; i += 2) { - qsb.add(qLits[i], qLits[i + 1]); + String[] qEntries = operationBinding.queryLiteralEntries(); + for (int i = 0; i < qKeys.length; i++) { + qsb.addPreEncoded(qKeys[i], qEntries[i]); } } diff --git a/io/src/main/java/software/amazon/smithy/java/io/uri/QueryStringBuilder.java b/io/src/main/java/software/amazon/smithy/java/io/uri/QueryStringBuilder.java index e3bc7f203..51e92647e 100644 --- a/io/src/main/java/software/amazon/smithy/java/io/uri/QueryStringBuilder.java +++ b/io/src/main/java/software/amazon/smithy/java/io/uri/QueryStringBuilder.java @@ -79,6 +79,23 @@ public void addForQueryParams(String key, String value) { } } + /** + * Append a pre-encoded {@code "key=value"} entry to the query string and register its raw key with the + * {@code @httpQuery} dedupe set so a subsequent {@link #addForQueryParams} with the same key will be skipped. + * + * @param rawKey raw (unencoded) key, used only for the dedupe set. + * @param encodedKeyEqValue {@code "encodedKey=encodedValue"} as a single ready-to-append string. + */ + public void addPreEncoded(String rawKey, String encodedKeyEqValue) { + if (!empty) { + builder.append('&'); + } else { + empty = false; + } + builder.append(encodedKeyEqValue); + httpQueryKeys.add(rawKey); + } + private void append(String key, String value) { if (!empty) { builder.append('&');