Skip to content

Commit 7fcd04c

Browse files
committed
Pool HTTP codec path and cache empty-body output
1 parent 59b59a7 commit 7fcd04c

3 files changed

Lines changed: 124 additions & 43 deletions

File tree

http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingDeserializer.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,16 @@ private <T> void readResponseStruct(
129129
readHeaderBindings(
130130
state,
131131
structMemberConsumer,
132-
rb.listHeaderMembers(),
133-
rb.listHeaderNames(),
134-
rb.scalarHeadersByName(),
135-
rb.prefixHeadersMembers());
132+
rb.listHeaderMembers,
133+
rb.listHeaderNames,
134+
rb.scalarHeadersByName,
135+
rb.prefixHeadersMembers);
136136

137-
if (rb.statusMember() != null) {
138-
structMemberConsumer.accept(state, rb.statusMember(), new ResponseStatusDeserializer(responseStatus));
137+
if (rb.statusMember != null) {
138+
structMemberConsumer.accept(state, rb.statusMember, new ResponseStatusDeserializer(responseStatus));
139139
}
140140

141-
readPayloadAndBody(schema, state, structMemberConsumer, rb.payloadMember(), rb.hasBody(), rb.bindings());
141+
readPayloadAndBody(schema, state, structMemberConsumer, rb.payloadMember, rb.hasBody, rb.bindings);
142142
}
143143

144144
/**

http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSchemaExtensions.java

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package software.amazon.smithy.java.http.binding;
77

8+
import java.nio.ByteBuffer;
89
import java.util.ArrayList;
910
import java.util.HashMap;
1011
import java.util.HashSet;
@@ -14,7 +15,10 @@
1415
import software.amazon.smithy.java.core.schema.Schema;
1516
import software.amazon.smithy.java.core.schema.SchemaExtensionKey;
1617
import software.amazon.smithy.java.core.schema.SchemaExtensionProvider;
18+
import software.amazon.smithy.java.core.schema.SerializableStruct;
1719
import software.amazon.smithy.java.core.schema.TraitKey;
20+
import software.amazon.smithy.java.core.serde.Codec;
21+
import software.amazon.smithy.java.core.serde.ShapeSerializer;
1822
import software.amazon.smithy.java.core.serde.TimestampFormatter;
1923
import software.amazon.smithy.java.http.api.HeaderName;
2024
import software.amazon.smithy.model.shapes.ShapeType;
@@ -246,29 +250,102 @@ PathSerializer pathSerializer(HttpTrait httpTrait, Schema inputSchema) {
246250
boolean writeBody(boolean omitEmptyPayload) {
247251
return hasBody || (!omitEmptyPayload && !hasPayload);
248252
}
253+
254+
// Lazy cache for the codec output of an empty struct (no body members + no payload + force-write-empty).
255+
// Bytes are determined by codec + schema, so caching once and duplicating views per request is safe.
256+
private volatile ByteBuffer cachedEmptyBody;
257+
258+
ByteBuffer emptyBody(Codec codec, Schema schema) {
259+
var b = cachedEmptyBody;
260+
if (b == null) {
261+
b = codec.serialize(new EmptyStruct(schema));
262+
cachedEmptyBody = b;
263+
}
264+
return b.duplicate();
265+
}
266+
}
267+
268+
/**
269+
* A no-member {@link SerializableStruct} used to seed the codec when caching the empty-body
270+
* representation for {@link RequestBinding#emptyBody} / {@link ResponseBinding#emptyBody}.
271+
*/
272+
private record EmptyStruct(Schema schema) implements SerializableStruct {
273+
@Override
274+
public void serializeMembers(ShapeSerializer serializer) {
275+
// no members
276+
}
277+
278+
@Override
279+
public <T> T getMemberValue(Schema member) {
280+
return null;
281+
}
249282
}
250283

251284
/**
252285
* Pre-computed response-direction binding data for one structure / union schema. Built lazily
253286
* by {@link StructBindings#response()} on first response-side access.
254287
*/
255-
record ResponseBinding(
256-
Binding[] bindings,
257-
MemberBinding[] memberBindings,
258-
Map<String, Schema> scalarHeadersByName,
259-
Schema[] listHeaderMembers,
260-
HeaderName[] listHeaderNames,
261-
Schema[] prefixHeadersMembers,
262-
Schema payloadMember,
263-
Schema statusMember,
264-
boolean hasBody,
265-
boolean hasPayload,
266-
Set<String> headerWireNames,
267-
int headerCount,
268-
int defaultStatus) {
288+
static final class ResponseBinding {
289+
final Binding[] bindings;
290+
final MemberBinding[] memberBindings;
291+
final Map<String, Schema> scalarHeadersByName;
292+
final Schema[] listHeaderMembers;
293+
final HeaderName[] listHeaderNames;
294+
final Schema[] prefixHeadersMembers;
295+
final Schema payloadMember;
296+
final Schema statusMember;
297+
final boolean hasBody;
298+
final boolean hasPayload;
299+
final Set<String> headerWireNames;
300+
final int headerCount;
301+
// Default response status from @httpError or @error trait, or -1 when neither is present.
302+
final int defaultStatus;
303+
// Lazy cache for the codec output of an empty struct (no body members + no payload + force-write-empty).
304+
// Bytes are determined by codec + schema, so caching once and duplicating views per request is safe.
305+
private volatile ByteBuffer cachedEmptyBody;
306+
307+
ResponseBinding(
308+
Binding[] bindings,
309+
MemberBinding[] memberBindings,
310+
Map<String, Schema> scalarHeadersByName,
311+
Schema[] listHeaderMembers,
312+
HeaderName[] listHeaderNames,
313+
Schema[] prefixHeadersMembers,
314+
Schema payloadMember,
315+
Schema statusMember,
316+
boolean hasBody,
317+
boolean hasPayload,
318+
Set<String> headerWireNames,
319+
int headerCount,
320+
int defaultStatus
321+
) {
322+
this.bindings = bindings;
323+
this.memberBindings = memberBindings;
324+
this.scalarHeadersByName = scalarHeadersByName;
325+
this.listHeaderMembers = listHeaderMembers;
326+
this.listHeaderNames = listHeaderNames;
327+
this.prefixHeadersMembers = prefixHeadersMembers;
328+
this.payloadMember = payloadMember;
329+
this.statusMember = statusMember;
330+
this.hasBody = hasBody;
331+
this.hasPayload = hasPayload;
332+
this.headerWireNames = headerWireNames;
333+
this.headerCount = headerCount;
334+
this.defaultStatus = defaultStatus;
335+
}
336+
269337
boolean writeBody(boolean omitEmptyPayload) {
270338
return hasBody || (!omitEmptyPayload && !hasPayload);
271339
}
340+
341+
ByteBuffer emptyBody(Codec codec, Schema schema) {
342+
var b = cachedEmptyBody;
343+
if (b == null) {
344+
b = codec.serialize(new EmptyStruct(schema));
345+
cachedEmptyBody = b;
346+
}
347+
return b.duplicate();
348+
}
272349
}
273350

274351
/**

http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import software.amazon.smithy.java.http.api.HeaderName;
3131
import software.amazon.smithy.java.http.api.HttpHeaders;
3232
import software.amazon.smithy.java.http.api.ModifiableHttpHeaders;
33-
import software.amazon.smithy.java.io.ByteBufferOutputStream;
3433
import software.amazon.smithy.java.io.ByteBufferUtils;
3534
import software.amazon.smithy.java.io.datastream.DataStream;
3635
import software.amazon.smithy.java.io.uri.QueryStringBuilder;
@@ -72,8 +71,8 @@ final class HttpBindingSerializer extends SpecificShapeSerializer implements Sha
7271
// Initialized in writeStruct. Empty until writeStruct is called.
7372
private Set<String> namesFromHttpHeader = Set.of();
7473

75-
private ShapeSerializer shapeBodySerializer;
76-
private ByteBufferOutputStream shapeBodyOutput;
74+
// Body bytes from the pooled codec.serialize() path.
75+
private ByteBuffer shapeBodyBuffer;
7776
private DataStream httpPayload;
7877
private EventStream<? extends SerializableStruct> eventStream;
7978
private int responseStatus;
@@ -111,26 +110,36 @@ public void writeStruct(Schema schema, SerializableStruct struct) {
111110
outerStruct = struct;
112111

113112
boolean writeBody;
113+
boolean hasBodyMembers;
114114
int headerCount;
115115
if (isResponse) {
116116
var resp = bindings.response();
117-
activeBindings = resp.bindings();
118-
memberBindings = resp.memberBindings();
119-
namesFromHttpHeader = resp.headerWireNames();
120-
headerCount = resp.headerCount();
121-
if (resp.defaultStatus() != -1) {
122-
responseStatus = resp.defaultStatus();
117+
activeBindings = resp.bindings;
118+
memberBindings = resp.memberBindings;
119+
namesFromHttpHeader = resp.headerWireNames;
120+
headerCount = resp.headerCount;
121+
hasBodyMembers = resp.hasBody;
122+
if (resp.defaultStatus != -1) {
123+
responseStatus = resp.defaultStatus;
123124
}
124125
writeBody = allowEmptyStructPayload || resp.writeBody(omitEmptyPayload);
126+
if (writeBody && !hasBodyMembers) {
127+
// No body members: fixed empty struct bytes, cached on the binding.
128+
shapeBodyBuffer = resp.emptyBody(payloadCodec, schema);
129+
}
125130
} else {
126131
var req = bindings.request();
127132
activeBindings = req.bindings;
128133
memberBindings = req.memberBindings;
129134
namesFromHttpHeader = req.headerWireNames;
130135
headerCount = req.headerCount;
136+
hasBodyMembers = req.hasBody;
131137
contentTypeHeaderInInput = req.inputContentTypeHeader;
132138
pathSerializer = req.pathSerializer(operationBinding.httpTrait(), schema);
133139
writeBody = allowEmptyStructPayload || req.writeBody(omitEmptyPayload);
140+
if (writeBody && !hasBodyMembers) {
141+
shapeBodyBuffer = req.emptyBody(payloadCodec, schema);
142+
}
134143
}
135144

136145
headers = HttpHeaders.ofModifiable(headerCount);
@@ -145,13 +154,10 @@ public void writeStruct(Schema schema, SerializableStruct struct) {
145154
}
146155

147156
if (writeBody) {
148-
shapeBodyOutput = new ByteBufferOutputStream();
149-
shapeBodySerializer = payloadCodec.createSerializer(shapeBodyOutput);
150-
// Serialize only the body members to the codec, going through writeStruct so the codec emits the
151-
// struct envelope (e.g. JSON {...}). The BodyMemberSerializer routes each member to the codec's
152-
// per-member serializer (passed inside writeStruct) when its binding is BODY, and to a null serializer
153-
// otherwise so non-body members (headers, labels, etc.) don't leak into the body.
154-
shapeBodySerializer.writeStruct(schema, new StructBodyProxy(struct, activeBindings));
157+
if (hasBodyMembers) {
158+
shapeBodyBuffer = payloadCodec.serialize(new StructBodyProxy(struct, activeBindings));
159+
}
160+
// Empty-body case (shapeBodyBuffer was set above from the cached empty bytes).
155161
headers.setHeader(HeaderName.CONTENT_TYPE, payloadMediaType);
156162
}
157163

@@ -179,9 +185,7 @@ private void prepareError(Schema schema) {
179185

180186
@Override
181187
public void flush() {
182-
if (shapeBodySerializer != null) {
183-
shapeBodySerializer.flush();
184-
}
188+
// Body bytes are produced eagerly by payloadCodec.serialize(); nothing to flush here.
185189
}
186190

187191
void setHttpPayload(Schema schema, DataStream value) {
@@ -226,14 +230,14 @@ private QueryStringBuilder queryStringParams() {
226230
}
227231

228232
boolean hasBody() {
229-
return shapeBodyOutput != null || httpPayload != null;
233+
return shapeBodyBuffer != null || httpPayload != null;
230234
}
231235

232236
DataStream getBody() {
233237
if (httpPayload != null) {
234238
return httpPayload;
235-
} else if (shapeBodyOutput != null) {
236-
return DataStream.ofByteBuffer(shapeBodyOutput.toByteBuffer(), payloadMediaType);
239+
} else if (shapeBodyBuffer != null) {
240+
return DataStream.ofByteBuffer(shapeBodyBuffer, payloadMediaType);
237241
} else {
238242
return DataStream.ofEmpty();
239243
}

0 commit comments

Comments
 (0)