Skip to content

Commit 6d4ba3b

Browse files
committed
Add http servlet roundtrip benchmarks
1 parent 0537ec7 commit 6d4ba3b

14 files changed

+1543
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.benchmark.apicall.protocol;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.net.URI;
21+
import org.eclipse.jetty.server.Connector;
22+
import org.eclipse.jetty.server.Server;
23+
import org.eclipse.jetty.server.ServerConnector;
24+
import org.eclipse.jetty.servlet.ServletContextHandler;
25+
import org.eclipse.jetty.servlet.ServletHolder;
26+
import software.amazon.awssdk.benchmark.utils.BenchmarkUtils;
27+
import software.amazon.awssdk.utils.IoUtils;
28+
29+
/**
30+
* Lightweight Jetty server for protocol roundtrip benchmarks.
31+
*/
32+
class ProtocolRoundtripServer {
33+
34+
private final Server server;
35+
private final int port;
36+
37+
ProtocolRoundtripServer(ProtocolRoundtripServlet servlet) throws IOException {
38+
port = BenchmarkUtils.getUnusedPort();
39+
server = new Server();
40+
ServerConnector connector = new ServerConnector(server);
41+
connector.setPort(port);
42+
server.setConnectors(new Connector[] {connector});
43+
44+
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
45+
context.addServlet(new ServletHolder(servlet), "/*");
46+
server.setHandler(context);
47+
}
48+
49+
void start() throws Exception {
50+
server.start();
51+
}
52+
53+
void stop() throws Exception {
54+
server.stop();
55+
}
56+
57+
URI getHttpUri() {
58+
return URI.create("http://localhost:" + port);
59+
}
60+
61+
static byte[] loadFixture(String path) throws IOException {
62+
try (InputStream is = ProtocolRoundtripServer.class.getClassLoader()
63+
.getResourceAsStream("fixtures/" + path)) {
64+
if (is == null) {
65+
throw new IOException("Fixture not found: fixtures/" + path);
66+
}
67+
return IoUtils.toByteArray(is);
68+
}
69+
}
70+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.benchmark.apicall.protocol;
17+
18+
import java.io.IOException;
19+
import java.io.OutputStream;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import javax.servlet.http.HttpServlet;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
25+
26+
/**
27+
* Minimal servlet for protocol roundtrip benchmarks. Returns pre-loaded canned responses
28+
* with zero request inspection overhead. Routes are matched by URI prefix or X-Amz-Target header.
29+
*/
30+
class ProtocolRoundtripServlet extends HttpServlet {
31+
32+
private final Map<String, CannedResponse> targetRoutes = new ConcurrentHashMap<>();
33+
private final Map<String, CannedResponse> uriRoutes = new ConcurrentHashMap<>();
34+
private CannedResponse defaultResponse;
35+
36+
/**
37+
* Register a route matched by X-Amz-Target header value.
38+
*/
39+
ProtocolRoundtripServlet routeByTarget(String target, String contentType, byte[] body) {
40+
targetRoutes.put(target, new CannedResponse(contentType, body));
41+
return this;
42+
}
43+
44+
/**
45+
* Register a route matched by URI prefix.
46+
*/
47+
ProtocolRoundtripServlet routeByUri(String uriPrefix, String contentType, byte[] body) {
48+
uriRoutes.put(uriPrefix, new CannedResponse(contentType, body));
49+
return this;
50+
}
51+
52+
/**
53+
* Register a route matched by URI prefix with additional response headers.
54+
*/
55+
ProtocolRoundtripServlet routeByUri(String uriPrefix, String contentType, byte[] body,
56+
Map<String, String> headers) {
57+
uriRoutes.put(uriPrefix, new CannedResponse(contentType, body, headers));
58+
return this;
59+
}
60+
61+
/**
62+
* Default response when no route matches.
63+
*/
64+
ProtocolRoundtripServlet defaultRoute(String contentType, byte[] body) {
65+
this.defaultResponse = new CannedResponse(contentType, body);
66+
return this;
67+
}
68+
69+
@Override
70+
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
71+
// Consume request body to simulate real HTTP exchange
72+
byte[] buf = new byte[8192];
73+
while (req.getInputStream().read(buf) != -1) {
74+
// drain
75+
}
76+
77+
CannedResponse canned = resolve(req);
78+
if (canned == null) {
79+
resp.sendError(404);
80+
return;
81+
}
82+
83+
resp.setStatus(200);
84+
resp.setContentType(canned.contentType);
85+
resp.setContentLength(canned.body.length);
86+
resp.setHeader("x-amzn-RequestId", "benchmark-request-id");
87+
if (canned.headers != null) {
88+
canned.headers.forEach(resp::setHeader);
89+
}
90+
try (OutputStream os = resp.getOutputStream()) {
91+
os.write(canned.body);
92+
}
93+
}
94+
95+
private CannedResponse resolve(HttpServletRequest req) {
96+
// Try X-Amz-Target first (JSON/CBOR protocols)
97+
String target = req.getHeader("X-Amz-Target");
98+
if (target != null) {
99+
CannedResponse r = targetRoutes.get(target);
100+
if (r != null) {
101+
return r;
102+
}
103+
}
104+
105+
// Try URI prefix match (REST protocols)
106+
String uri = req.getRequestURI();
107+
for (Map.Entry<String, CannedResponse> entry : uriRoutes.entrySet()) {
108+
if (uri.contains(entry.getKey())) {
109+
return entry.getValue();
110+
}
111+
}
112+
113+
return defaultResponse;
114+
}
115+
116+
private static class CannedResponse {
117+
final String contentType;
118+
final byte[] body;
119+
final Map<String, String> headers;
120+
121+
CannedResponse(String contentType, byte[] body) {
122+
this(contentType, body, null);
123+
}
124+
125+
CannedResponse(String contentType, byte[] body, Map<String, String> headers) {
126+
this.contentType = contentType;
127+
this.body = body;
128+
this.headers = headers;
129+
}
130+
}
131+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.benchmark.apicall.protocol;
17+
18+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
19+
import com.amazonaws.auth.BasicAWSCredentials;
20+
import com.amazonaws.client.builder.AwsClientBuilder;
21+
import com.amazonaws.protocol.rpcv2cbor.SdkStructuredCborFactory;
22+
import com.amazonaws.protocol.rpcv2cbor.StructuredRpcV2CborGenerator;
23+
import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
24+
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClientBuilder;
25+
import com.amazonaws.services.cloudwatch.model.GetMetricDataRequest;
26+
import com.amazonaws.services.cloudwatch.model.Metric;
27+
import com.amazonaws.services.cloudwatch.model.MetricDataQuery;
28+
import com.amazonaws.services.cloudwatch.model.MetricStat;
29+
import java.util.Date;
30+
import java.util.concurrent.TimeUnit;
31+
import org.openjdk.jmh.annotations.Benchmark;
32+
import org.openjdk.jmh.annotations.BenchmarkMode;
33+
import org.openjdk.jmh.annotations.Fork;
34+
import org.openjdk.jmh.annotations.Level;
35+
import org.openjdk.jmh.annotations.Measurement;
36+
import org.openjdk.jmh.annotations.Mode;
37+
import org.openjdk.jmh.annotations.OutputTimeUnit;
38+
import org.openjdk.jmh.annotations.Scope;
39+
import org.openjdk.jmh.annotations.Setup;
40+
import org.openjdk.jmh.annotations.State;
41+
import org.openjdk.jmh.annotations.TearDown;
42+
import org.openjdk.jmh.annotations.Warmup;
43+
import org.openjdk.jmh.infra.Blackhole;
44+
45+
/**
46+
* V1 roundtrip benchmark for SmithyRpcV2 CBOR protocol using CloudWatch GetMetricData via HTTP servlet.
47+
*/
48+
@State(Scope.Benchmark)
49+
@Warmup(iterations = 5)
50+
@Measurement(iterations = 5)
51+
@Fork(2)
52+
@BenchmarkMode(Mode.Throughput)
53+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
54+
public class V1CborRoundtripBenchmark {
55+
56+
private ProtocolRoundtripServer server;
57+
private AmazonCloudWatch client;
58+
private GetMetricDataRequest request;
59+
60+
@Setup(Level.Trial)
61+
public void setup() throws Exception {
62+
byte[] response = createCborResponseFixture();
63+
64+
ProtocolRoundtripServlet servlet = new ProtocolRoundtripServlet()
65+
.defaultRoute("application/cbor", response);
66+
67+
server = new ProtocolRoundtripServer(servlet);
68+
server.start();
69+
70+
client = AmazonCloudWatchClientBuilder.standard()
71+
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(
72+
server.getHttpUri().toString(), "us-east-1"))
73+
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("test", "test")))
74+
.build();
75+
76+
Date end = Date.from(java.time.Instant.parse("2026-03-09T00:00:00Z"));
77+
Date start = Date.from(java.time.Instant.parse("2026-03-09T00:00:00Z").minusSeconds(3600));
78+
request = new GetMetricDataRequest()
79+
.withStartTime(start)
80+
.withEndTime(end)
81+
.withMaxDatapoints(1000)
82+
.withMetricDataQueries(
83+
new MetricDataQuery()
84+
.withId("cpu")
85+
.withMetricStat(new MetricStat()
86+
.withMetric(new Metric()
87+
.withNamespace("AWS/EC2")
88+
.withMetricName("CPUUtilization"))
89+
.withPeriod(300)
90+
.withStat("Average"))
91+
.withReturnData(true));
92+
}
93+
94+
@TearDown(Level.Trial)
95+
public void tearDown() throws Exception {
96+
client.shutdown();
97+
server.stop();
98+
}
99+
100+
@Benchmark
101+
public void getMetricData(Blackhole bh) {
102+
bh.consume(client.getMetricData(request));
103+
}
104+
105+
private static byte[] createCborResponseFixture() {
106+
StructuredRpcV2CborGenerator gen =
107+
SdkStructuredCborFactory.SDK_CBOR_FACTORY.createWriter("application/cbor");
108+
gen.writeStartObject();
109+
gen.writeFieldName("MetricDataResults");
110+
gen.writeStartArray();
111+
gen.writeStartObject();
112+
gen.writeFieldName("Id");
113+
gen.writeValue("cpu");
114+
gen.writeFieldName("Label");
115+
gen.writeValue("CPUUtilization");
116+
gen.writeFieldName("StatusCode");
117+
gen.writeValue("Complete");
118+
gen.writeFieldName("Timestamps");
119+
gen.writeStartArray();
120+
long base = 1772611200L;
121+
for (int i = 0; i < 12; i++) {
122+
gen.writeValue((double) ((base + i * 300) * 1000));
123+
}
124+
gen.writeEndArray();
125+
gen.writeFieldName("Values");
126+
gen.writeStartArray();
127+
for (int i = 0; i < 12; i++) {
128+
gen.writeValue(45.2 + i * 1.1);
129+
}
130+
gen.writeEndArray();
131+
gen.writeFieldName("Messages");
132+
gen.writeStartArray();
133+
gen.writeEndArray();
134+
gen.writeEndObject();
135+
gen.writeEndArray();
136+
gen.writeFieldName("Messages");
137+
gen.writeStartArray();
138+
gen.writeEndArray();
139+
gen.writeEndObject();
140+
return gen.getBytes();
141+
}
142+
}

0 commit comments

Comments
 (0)