diff --git a/test/http-client-benchmarks/pom.xml b/test/http-client-benchmarks/pom.xml
index 884a7b6490a4..eae31142484a 100644
--- a/test/http-client-benchmarks/pom.xml
+++ b/test/http-client-benchmarks/pom.xml
@@ -74,6 +74,24 @@
${awsjavasdk.version}-PREVIEW
+
+ software.amazon.awssdk
+ url-connection-client
+ ${awsjavasdk.version}
+
+
+
+ software.amazon.awssdk
+ aws-crt-client
+ ${awsjavasdk.version}
+
+
+
+ software.amazon.awssdk
+ netty-nio-client
+ ${awsjavasdk.version}
+
+
org.apache.logging.log4j
log4j-api
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/AsyncVirtualThreadBenchmark.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/AsyncVirtualThreadBenchmark.java
new file mode 100644
index 000000000000..827aa547144a
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/AsyncVirtualThreadBenchmark.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark;
+
+import static software.amazon.awssdk.benchmark.apache5.utility.BenchmarkUtilities.isJava21OrHigher;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.benchmark.core.ObjectSize;
+import software.amazon.awssdk.benchmark.core.S3BenchmarkHelper;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.async.AsyncResponseTransformer;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.IoUtils;
+import software.amazon.awssdk.utils.JavaSystemSetting;
+
+/**
+ * Async http client benchmark using virtual threads. This class requires Java 21+.
+ */
+@Fork(jvmArgsAppend = "-Djdk.tracePinnedThreads=full")
+@State(Scope.Benchmark)
+public class AsyncVirtualThreadBenchmark {
+ // We redirect standard out to a file for the -Djdk.tracePinnedThreads=full option. When virtual threads become pinned,
+ // the JDK will print out the stacktrace through standard out. However, because JMH runs benchmarks in a forked JVM
+ // (unless you specify -f 0, which is not recommended by JMH), that output is lost. Redirect standard out to a file so
+ // that any time a thread is pinned, the stack trace is written to the file instead,which can be inspected after the
+ // benchmark run.
+ static {
+ try {
+ Path tmp = Paths.get(AsyncVirtualThreadBenchmark.class.getSimpleName() + "-stdout-" + UUID.randomUUID() + ".log");
+ PrintStream fileOut = new PrintStream(
+ Files.newOutputStream(tmp, StandardOpenOption.APPEND, StandardOpenOption.CREATE));
+ System.setOut(fileOut);
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to create STDOUT file", e);
+ }
+ }
+
+ public enum Client {
+ Netty,
+ Crt
+ }
+
+ @Param("50")
+ private int maxConnections;
+
+ @Param("SMALL")
+ private ObjectSize objectSize;
+
+ @Param({"Netty", "Crt"})
+ private Client client;
+
+ private S3AsyncClient s3AsyncClient;
+ private S3BenchmarkHelper benchmark;
+ private ExecutorService virtualThreadExecutor;
+ private String putKeyPrefix;
+
+ @Setup(Level.Trial)
+ public void setup() {
+ if (!isJava21OrHigher()) {
+ throw new UnsupportedOperationException(
+ "Virtual threads require Java 21 or higher. Current version: " + JavaSystemSetting.JAVA_VERSION);
+ }
+
+ SdkAsyncHttpClient.Builder> httpClientBuilder = httpClientBuilder();
+
+ s3AsyncClient = S3AsyncClient.builder()
+ .region(Region.US_WEST_2)
+ .credentialsProvider(DefaultCredentialsProvider.create())
+ .httpClient(configure(httpClientBuilder))
+ .build();
+
+ String benchmarkName = AsyncVirtualThreadBenchmark.class.getSimpleName();
+
+ benchmark = new S3BenchmarkHelper(benchmarkName, s3AsyncClient);
+ benchmark.setup();
+
+ virtualThreadExecutor = createVirtualThreadExecutor();
+
+ putKeyPrefix = benchmarkName + "-";
+ }
+
+ private SdkAsyncHttpClient configure(SdkAsyncHttpClient.Builder> builder) {
+ AttributeMap config = AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.MAX_CONNECTIONS, maxConnections)
+ .build();
+
+ return builder.buildWithDefaults(config);
+ }
+
+ private ExecutorService createVirtualThreadExecutor() {
+ try {
+ // Use reflection to call Executors.newVirtualThreadPerTaskExecutor()
+ Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
+ return (ExecutorService) method.invoke(null);
+ } catch (NoSuchMethodException e) {
+ throw new UnsupportedOperationException(
+ "Virtual threads are not available in this Java version. " +
+ "This benchmark requires Java 21 or higher.", e);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Failed to create virtual thread executor", e);
+ }
+ }
+
+ @Benchmark
+ public void getObject(Blackhole blackhole) {
+ safeExecute(() -> {
+ ResponseInputStream object = s3AsyncClient.getObject(
+ r -> r.bucket(benchmark.bucketName()).key(benchmark.objKey(objectSize)),
+ AsyncResponseTransformer.toBlockingInputStream()).join();
+ blackhole.consume(object.response());
+ IoUtils.drainInputStream(object);
+ });
+ }
+
+ @Benchmark
+ public void putObject(Blackhole blackhole) {
+ String jmhThreadName = Thread.currentThread().getName();
+ safeExecute(() -> {
+ PutObjectResponse response = s3AsyncClient.putObject(
+ r -> r.bucket(benchmark.bucketName()).key(putKeyPrefix + jmhThreadName),
+ benchmark.asyncRequestBody(objectSize)).join();
+ blackhole.consume(response);
+ });
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ if (virtualThreadExecutor != null) {
+ virtualThreadExecutor.shutdown();
+ try {
+ if (!virtualThreadExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+ virtualThreadExecutor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ virtualThreadExecutor.shutdownNow();
+ }
+ }
+
+ if (benchmark != null) {
+ benchmark.cleanup();
+ }
+
+ if (s3AsyncClient != null) {
+ s3AsyncClient.close();
+ }
+ }
+
+ private void safeExecute(Runnable runnable) {
+ try {
+ virtualThreadExecutor.submit(runnable).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException("Error during execution", e);
+ }
+ }
+
+ private SdkAsyncHttpClient.Builder> httpClientBuilder() {
+ switch (client) {
+ case Netty:
+ return NettyNioAsyncHttpClient.builder();
+ case Crt:
+ return AwsCrtAsyncHttpClient.builder();
+ default:
+ throw new IllegalArgumentException("Unknown HTTP client: " + client);
+ }
+ }
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/UnifiedBenchmarkRunner.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/UnifiedBenchmarkRunner.java
index 8ee83735e204..f0f14a976db6 100644
--- a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/UnifiedBenchmarkRunner.java
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/UnifiedBenchmarkRunner.java
@@ -31,7 +31,6 @@
import org.openjdk.jmh.runner.options.OptionsBuilder;
import software.amazon.awssdk.benchmark.apache4.Apache4Benchmark;
import software.amazon.awssdk.benchmark.apache5.Apache5Benchmark;
-import software.amazon.awssdk.benchmark.apache5.Apache5VirtualBenchmark;
import software.amazon.awssdk.benchmark.core.BenchmarkResult;
import software.amazon.awssdk.benchmark.metrics.CloudWatchMetricsPublisher;
import software.amazon.awssdk.regions.Region;
@@ -76,7 +75,7 @@ public static void main(String[] args) throws Exception {
// Only run virtual threads benchmark if Java 21+
if (isJava21OrHigher()) {
logger.info(() -> "Running Apache5 with virtual threads...");
- allResults.addAll(runBenchmark("Apache5-Virtual", Apache5VirtualBenchmark.class));
+ allResults.addAll(runBenchmark("Apache5-Virtual", VirtualThreadBenchmark.class));
} else {
logger.info(() -> "Skipping virtual threads benchmark - requires Java 21 or higher (current: " +
JavaSystemSetting.JAVA_VERSION.getStringValueOrThrow() + ")");
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/VirtualThreadBenchmark.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/VirtualThreadBenchmark.java
new file mode 100644
index 000000000000..e1720f4223e4
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/VirtualThreadBenchmark.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark;
+
+import static software.amazon.awssdk.benchmark.apache5.utility.BenchmarkUtilities.isJava21OrHigher;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.benchmark.core.ObjectSize;
+import software.amazon.awssdk.benchmark.core.S3BenchmarkHelper;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.ResponseTransformer;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.http.apache5.Apache5HttpClient;
+import software.amazon.awssdk.http.crt.AwsCrtHttpClient;
+import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.IoUtils;
+import software.amazon.awssdk.utils.JavaSystemSetting;
+
+/**
+ * Http client benchmark using virtual threads. This class requires Java 21+.
+ */
+@Fork(jvmArgsAppend = "-Djdk.tracePinnedThreads=full")
+@State(Scope.Benchmark)
+public class VirtualThreadBenchmark {
+ static {
+ // We redirect standard out to a file for the -Djdk.tracePinnedThreads=full option. When virtual threads become pinned,
+ // the JDK will print out the stacktrace through standard out. However, because JMH runs benchmarks in a forked JVM
+ // (unless you specify -f 0, which is not recommended by JMH), that output is lost. Redirect standard out to a file so
+ // that any time a thread is pinned, the stack trace is written to the file instead,which can be inspected after the
+ // benchmark run.
+ try {
+ Path tmp = Paths.get(VirtualThreadBenchmark.class.getSimpleName() + "-stdout-" + UUID.randomUUID() + ".log");
+ PrintStream fileOut = new PrintStream(
+ Files.newOutputStream(tmp, StandardOpenOption.APPEND, StandardOpenOption.CREATE));
+ System.setOut(fileOut);
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to create STDOUT file", e);
+ }
+ }
+
+ public enum Client {
+ Apache5,
+ Apache4,
+ UrlConnection,
+ Crt
+ }
+
+ @Param("50")
+ private int maxConnections;
+
+ @Param("SMALL")
+ private ObjectSize objectSize;
+
+ @Param({"Apache5", "UrlConnection", "Crt"})
+ // Note: We know pinning happens with Apache4, so don't bother testing it by
+ // default
+ private Client client;
+
+ private S3Client s3Client;
+ private S3BenchmarkHelper benchmark;
+ private ExecutorService virtualThreadExecutor;
+ private String putKeyPrefix;
+
+ @Setup(Level.Trial)
+ public void setup() {
+ if (!isJava21OrHigher()) {
+ throw new UnsupportedOperationException(
+ "Virtual threads require Java 21 or higher. Current version: " + JavaSystemSetting.JAVA_VERSION);
+ }
+
+ SdkHttpClient.Builder> httpClientBuilder = httpClientBuilder();
+
+ s3Client = S3Client.builder()
+ .region(Region.US_WEST_2)
+ .credentialsProvider(DefaultCredentialsProvider.create())
+ .httpClient(configure(httpClientBuilder))
+ .build();
+
+ String benchmarkName = VirtualThreadBenchmark.class.getSimpleName();
+
+ benchmark = new S3BenchmarkHelper(benchmarkName, s3Client);
+ benchmark.setup();
+
+ virtualThreadExecutor = createVirtualThreadExecutor();
+
+ putKeyPrefix = benchmarkName + "-";
+ }
+
+ private SdkHttpClient configure(SdkHttpClient.Builder> builder) {
+ AttributeMap config = AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.MAX_CONNECTIONS, maxConnections)
+ .build();
+
+ return builder.buildWithDefaults(config);
+ }
+
+ private ExecutorService createVirtualThreadExecutor() {
+ try {
+ // Use reflection to call Executors.newVirtualThreadPerTaskExecutor()
+ Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
+ return (ExecutorService) method.invoke(null);
+ } catch (NoSuchMethodException e) {
+ throw new UnsupportedOperationException(
+ "Virtual threads are not available in this Java version. " +
+ "This benchmark requires Java 21 or higher.", e);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Failed to create virtual thread executor", e);
+ }
+ }
+
+ @Benchmark
+ public void getObject(Blackhole blackhole) {
+ safeExecute(() -> {
+ ResponseInputStream object = s3Client.getObject(
+ r -> r.bucket(benchmark.bucketName()).key(benchmark.objKey(objectSize)),
+ ResponseTransformer.toInputStream());
+ blackhole.consume(object.response());
+ IoUtils.drainInputStream(object);
+ });
+ }
+
+ @Benchmark
+ public void putObject(Blackhole blackhole) {
+ String jmhThreadName = Thread.currentThread().getName();
+ safeExecute(() -> {
+ PutObjectResponse response = s3Client.putObject(
+ r -> r.bucket(benchmark.bucketName()).key(putKeyPrefix + jmhThreadName),
+ benchmark.requestBody(objectSize));
+ blackhole.consume(response);
+ });
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ if (virtualThreadExecutor != null) {
+ virtualThreadExecutor.shutdown();
+ try {
+ if (!virtualThreadExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+ virtualThreadExecutor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ virtualThreadExecutor.shutdownNow();
+ }
+ }
+
+ if (benchmark != null) {
+ benchmark.cleanup();
+ }
+
+ if (s3Client != null) {
+ s3Client.close();
+ }
+ }
+
+ private void safeExecute(Runnable runnable) {
+ try {
+ virtualThreadExecutor.submit(runnable).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException("Error during execution", e);
+ }
+ }
+
+ SdkHttpClient.Builder> httpClientBuilder() {
+ switch (client) {
+ case Apache5:
+ return Apache5HttpClient.builder();
+ case Apache4:
+ return ApacheHttpClient.builder();
+ case UrlConnection:
+ return UrlConnectionHttpClient.builder();
+ case Crt:
+ return AwsCrtHttpClient.builder();
+ default:
+ throw new IllegalArgumentException("Unknown HTTP client: " + client);
+ }
+ }
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache4/Apache4Benchmark.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache4/Apache4Benchmark.java
index 87dcb9ea84be..adec6b908736 100644
--- a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache4/Apache4Benchmark.java
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache4/Apache4Benchmark.java
@@ -37,11 +37,16 @@
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import software.amazon.awssdk.benchmark.core.CoreBenchmark;
-import software.amazon.awssdk.benchmark.core.S3BenchmarkImpl;
+import software.amazon.awssdk.benchmark.core.ObjectSize;
+import software.amazon.awssdk.benchmark.core.S3BenchmarkHelper;
+import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.Logger;
@BenchmarkMode(Mode.Throughput)
@@ -56,14 +61,14 @@ public class Apache4Benchmark implements CoreBenchmark {
@Param({"50"})
private int maxConnections;
- @Param({"5"})
- private int testDataInMB;
+ @Param("SMALL")
+ private ObjectSize objectSize;
@Param({"10"})
private int threadCount;
private S3Client s3Client;
- private S3BenchmarkImpl benchmark;
+ private S3BenchmarkHelper benchmarkHelper;
private ExecutorService platformThreadPool;
@Setup(Level.Trial)
@@ -84,9 +89,8 @@ public void setup() {
.httpClient(httpClient)
.build();
- // Initialize benchmark implementation
- benchmark = new S3BenchmarkImpl(s3Client, new byte[testDataInMB * 1024 * 1024]);
- benchmark.setup();
+ benchmarkHelper = new S3BenchmarkHelper(Apache4Benchmark.class.getSimpleName(), s3Client);
+ benchmarkHelper.setup();
// Platform thread pool for multi-threaded tests
platformThreadPool = Executors.newFixedThreadPool(threadCount, r -> {
@@ -102,13 +106,13 @@ public void setup() {
@Benchmark
@Override
public void simpleGet(Blackhole blackhole) throws Exception {
- benchmark.executeGet("medium", blackhole);
+ executeGet(blackhole);
}
@Benchmark
@Override
public void simplePut(Blackhole blackhole) throws Exception {
- benchmark.executePut("medium", blackhole);
+ executePut(blackhole);
}
@Benchmark
@@ -123,6 +127,20 @@ public void multiThreadedPut(Blackhole blackhole) throws Exception {
runMultiThreaded(platformThreadPool, threadCount, blackhole, false);
}
+ private void executeGet(Blackhole blackhole) {
+ ResponseInputStream object = s3Client.getObject(
+ r -> r.bucket(benchmarkHelper.bucketName()).key(benchmarkHelper.objKey(objectSize)));
+ blackhole.consume(object.response());
+ IoUtils.drainInputStream(object);
+ }
+
+ private void executePut(Blackhole blackhole) {
+ PutObjectResponse response = s3Client.putObject(
+ r -> r.bucket(benchmarkHelper.bucketName()).key("Apache4Benchmark-" + Thread.currentThread().getName()),
+ benchmarkHelper.requestBody(objectSize));
+ blackhole.consume(response);
+ }
+
protected void runMultiThreaded(ExecutorService executor, int threads,
Blackhole blackhole, boolean isGet) throws Exception {
List> futures = new ArrayList<>(threads);
@@ -131,9 +149,9 @@ protected void runMultiThreaded(ExecutorService executor, int threads,
futures.add(executor.submit(() -> {
try {
if (isGet) {
- benchmark.executeGet("medium", blackhole);
+ executeGet(blackhole);
} else {
- benchmark.executePut("medium", blackhole);
+ executePut(blackhole);
}
} catch (Exception e) {
throw new RuntimeException("Operation failed", e);
@@ -162,8 +180,8 @@ public void tearDown() {
}
}
- if (benchmark != null) {
- benchmark.cleanup();
+ if (benchmarkHelper != null) {
+ benchmarkHelper.cleanup();
}
if (s3Client != null) {
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5Benchmark.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5Benchmark.java
index f80569e86d9f..a398716d1ba3 100644
--- a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5Benchmark.java
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5Benchmark.java
@@ -38,11 +38,16 @@
import org.openjdk.jmh.infra.Blackhole;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.benchmark.core.CoreBenchmark;
-import software.amazon.awssdk.benchmark.core.S3BenchmarkImpl;
+import software.amazon.awssdk.benchmark.core.ObjectSize;
+import software.amazon.awssdk.benchmark.core.S3BenchmarkHelper;
+import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.apache5.Apache5HttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.Logger;
@BenchmarkMode(Mode.Throughput)
@@ -57,17 +62,14 @@ public class Apache5Benchmark implements CoreBenchmark {
@Param({"50"})
private int maxConnections;
- @Param({"5"})
- private int testDataInMB;
+ @Param("SMALL")
+ private ObjectSize objectSize;
@Param({"10"})
private int threadCount;
- @Param({"platform"})
- private String executorType;
-
private S3Client s3Client;
- private S3BenchmarkImpl benchmark;
+ private S3BenchmarkHelper benchmarkHelper;
private ExecutorService executorService;
@Setup(Level.Trial)
@@ -89,9 +91,8 @@ public void setup() {
.httpClient(httpClient)
.build();
- // Initialize benchmark implementation
- benchmark = new S3BenchmarkImpl(s3Client, new byte[this.testDataInMB * 1024 * 1024]);
- benchmark.setup();
+ benchmarkHelper = new S3BenchmarkHelper(Apache5Benchmark.class.getSimpleName(), s3Client);
+ benchmarkHelper.setup();
// Always use platform threads
executorService = Executors.newFixedThreadPool(threadCount, r -> {
@@ -106,15 +107,13 @@ public void setup() {
}
@Benchmark
- @Override
- public void simpleGet(Blackhole blackhole) throws Exception {
- benchmark.executeGet("medium", blackhole);
+ public void simpleGet(Blackhole blackhole) {
+ executeGet(blackhole);
}
@Benchmark
- @Override
- public void simplePut(Blackhole blackhole) throws Exception {
- benchmark.executePut("medium", blackhole);
+ public void simplePut(Blackhole blackhole) {
+ executePut(blackhole);
}
@Benchmark
@@ -125,7 +124,7 @@ public void multiThreadedGet(Blackhole blackhole) throws Exception {
for (int i = 0; i < threadCount; i++) {
futures.add(executorService.submit(() -> {
try {
- benchmark.executeGet("medium", blackhole);
+ executeGet(blackhole);
} catch (Exception e) {
throw new RuntimeException("GET operation failed", e);
}
@@ -146,7 +145,7 @@ public void multiThreadedPut(Blackhole blackhole) throws Exception {
for (int i = 0; i < threadCount; i++) {
futures.add(executorService.submit(() -> {
try {
- benchmark.executePut("medium", blackhole);
+ executePut(blackhole);
} catch (Exception e) {
throw new RuntimeException("PUT operation failed", e);
}
@@ -159,6 +158,20 @@ public void multiThreadedPut(Blackhole blackhole) throws Exception {
}
}
+ private void executeGet(Blackhole blackhole) {
+ ResponseInputStream object = s3Client.getObject(
+ r -> r.bucket(benchmarkHelper.bucketName()).key(benchmarkHelper.objKey(objectSize)));
+ blackhole.consume(object.response());
+ IoUtils.drainInputStream(object);
+ }
+
+ private void executePut(Blackhole blackhole) {
+ PutObjectResponse response = s3Client.putObject(
+ r -> r.bucket(benchmarkHelper.bucketName()).key("Apache5Benchmark-" + Thread.currentThread().getName()),
+ benchmarkHelper.requestBody(objectSize));
+ blackhole.consume(response);
+ }
+
@TearDown(Level.Trial)
public void tearDown() {
logger.info(() -> "Tearing down Apache5 benchmark");
@@ -174,8 +187,8 @@ public void tearDown() {
}
}
- if (benchmark != null) {
- benchmark.cleanup();
+ if (benchmarkHelper != null) {
+ benchmarkHelper.cleanup();
}
if (s3Client != null) {
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5VirtualBenchmark.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5VirtualBenchmark.java
deleted file mode 100644
index e0c228df3ca4..000000000000
--- a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apache5/Apache5VirtualBenchmark.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.awssdk.benchmark.apache5;
-
-import static software.amazon.awssdk.benchmark.apache5.utility.BenchmarkUtilities.isJava21OrHigher;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.BenchmarkMode;
-import org.openjdk.jmh.annotations.Fork;
-import org.openjdk.jmh.annotations.Level;
-import org.openjdk.jmh.annotations.Measurement;
-import org.openjdk.jmh.annotations.Mode;
-import org.openjdk.jmh.annotations.OutputTimeUnit;
-import org.openjdk.jmh.annotations.Param;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.Setup;
-import org.openjdk.jmh.annotations.State;
-import org.openjdk.jmh.annotations.TearDown;
-import org.openjdk.jmh.annotations.Warmup;
-import org.openjdk.jmh.infra.Blackhole;
-import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
-import software.amazon.awssdk.benchmark.core.CoreBenchmark;
-import software.amazon.awssdk.benchmark.core.S3BenchmarkImpl;
-import software.amazon.awssdk.http.SdkHttpClient;
-import software.amazon.awssdk.http.apache5.Apache5HttpClient;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.utils.JavaSystemSetting;
-import software.amazon.awssdk.utils.Logger;
-
-/**
- * Apache5 benchmark using virtual threads. This class requires Java 21+.
- */
-@BenchmarkMode(Mode.Throughput)
-@OutputTimeUnit(TimeUnit.SECONDS)
-@State(Scope.Benchmark)
-@Fork(value = 1, jvmArgs = {"-Xms2G", "-Xmx2G"})
-@Warmup(iterations = 3, time = 15, timeUnit = TimeUnit.SECONDS)
-@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
-public class Apache5VirtualBenchmark implements CoreBenchmark {
-
- private static final Logger logger = Logger.loggerFor(Apache5VirtualBenchmark.class);
-
- @Param({"50"})
- private int maxConnections;
-
- @Param({"5"})
- private int testDataInMB;
-
- @Param({"10"})
- private int threadCount;
-
- private S3Client s3Client;
- private S3BenchmarkImpl benchmark;
- private ExecutorService executorService;
-
-
-
- @Setup(Level.Trial)
- public void setup() {
- // Verify Java version
- String version = JavaSystemSetting.JAVA_VERSION.getStringValueOrThrow();
- // Update logging call to use Supplier pattern
- logger.info(() -> "Running on Java version: " + version);
-
- if (!isJava21OrHigher()) {
- throw new UnsupportedOperationException(
- "Virtual threads require Java 21 or higher. Current version: " + version);
- }
-
- // Update logging call to use Supplier pattern
- logger.info(() -> "Setting up Apache5 virtual threads benchmark with maxConnections=" + maxConnections);
-
- // Apache 5 HTTP client
- SdkHttpClient httpClient = Apache5HttpClient.builder()
- .connectionTimeout(Duration.ofSeconds(10))
- .socketTimeout(Duration.ofSeconds(30))
- .connectionAcquisitionTimeout(Duration.ofSeconds(10))
- .maxConnections(maxConnections)
- .build();
-
- // S3 client
- s3Client = S3Client.builder()
- .region(Region.US_WEST_2)
- .credentialsProvider(DefaultCredentialsProvider.create())
- .httpClient(httpClient)
- .build();
-
- // Initialize benchmark implementation
- benchmark = new S3BenchmarkImpl(s3Client, new byte[this.testDataInMB * 1024 * 1024]);
- benchmark.setup();
-
- // Create virtual thread executor
- executorService = createVirtualThreadExecutor();
- // Update logging call to use Supplier pattern
- logger.info(() -> "Using virtual thread executor");
-
- // Update logging call to use Supplier pattern
- logger.info(() -> "Apache5 virtual threads benchmark setup complete");
- }
-
- private ExecutorService createVirtualThreadExecutor() {
- try {
- // Use reflection to call Executors.newVirtualThreadPerTaskExecutor()
- Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
- return (ExecutorService) method.invoke(null);
- } catch (NoSuchMethodException e) {
- throw new UnsupportedOperationException(
- "Virtual threads are not available in this Java version. " +
- "This benchmark requires Java 21 or higher.", e);
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException("Failed to create virtual thread executor", e);
- }
- }
-
- @Benchmark
- @Override
- public void simpleGet(Blackhole blackhole) throws Exception {
- benchmark.executeGet("medium", blackhole);
- }
-
- @Benchmark
- @Override
- public void simplePut(Blackhole blackhole) throws Exception {
- benchmark.executePut("medium", blackhole);
- }
-
- @Benchmark
- @Override
- public void multiThreadedGet(Blackhole blackhole) throws Exception {
- List> futures = new ArrayList<>(threadCount);
-
- for (int i = 0; i < threadCount; i++) {
- futures.add(executorService.submit(() -> {
- try {
- benchmark.executeGet("medium", blackhole);
- } catch (Exception e) {
- throw new RuntimeException("GET operation failed", e);
- }
- }));
- }
-
- // Wait for all operations to complete
- for (Future> future : futures) {
- future.get();
- }
- }
-
- @Benchmark
- @Override
- public void multiThreadedPut(Blackhole blackhole) throws Exception {
- List> futures = new ArrayList<>(threadCount);
-
- for (int i = 0; i < threadCount; i++) {
- futures.add(executorService.submit(() -> {
- try {
- benchmark.executePut("medium", blackhole);
- } catch (Exception e) {
- throw new RuntimeException("PUT operation failed", e);
- }
- }));
- }
-
- // Wait for all operations to complete
- for (Future> future : futures) {
- future.get();
- }
- }
-
- @TearDown(Level.Trial)
- public void tearDown() {
- logger.info(() -> "Tearing down Apache5 virtual threads benchmark");
-
- if (executorService != null) {
- executorService.shutdown();
- try {
- if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
- executorService.shutdownNow();
- }
- } catch (InterruptedException e) {
- executorService.shutdownNow();
- }
- }
-
- if (benchmark != null) {
- benchmark.cleanup();
- }
-
- if (s3Client != null) {
- s3Client.close();
- }
- }
-}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/AsyncS3Wrapper.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/AsyncS3Wrapper.java
new file mode 100644
index 000000000000..71dd6c3c60eb
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/AsyncS3Wrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark.core;
+
+import java.nio.file.Path;
+import software.amazon.awssdk.core.async.AsyncRequestBody;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+public class AsyncS3Wrapper implements S3Wrapper {
+ private final S3AsyncClient s3;
+
+ public AsyncS3Wrapper(S3AsyncClient s3) {
+ this.s3 = s3;
+ }
+
+ @Override
+ public void createBucket(CreateBucketRequest request) {
+ s3.createBucket(request).join();
+ }
+
+ @Override
+ public void putObject(PutObjectRequest request, Path file) {
+ s3.putObject(request, AsyncRequestBody.fromFile(file)).join();
+ }
+
+ @Override
+ public ListObjectsV2Response listObjectsV2(ListObjectsV2Request request) {
+ return s3.listObjectsV2(request).join();
+ }
+
+ @Override
+ public void deleteObject(DeleteObjectRequest request) {
+ s3.deleteObject(request).join();
+ }
+
+ @Override
+ public void deleteBucket(DeleteBucketRequest request) {
+ s3.deleteBucket(request).join();
+ }
+
+ @Override
+ public void waitUntilBucketExists(String bucketName) {
+ s3.waiter().waitUntilBucketExists(r -> r.bucket(bucketName)).join();
+ }
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/ObjectSize.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/ObjectSize.java
new file mode 100644
index 000000000000..f9dafbcb7354
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/ObjectSize.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark.core;
+
+public enum ObjectSize {
+
+ SMALL(1024L * 1024),
+ MEDIUM(8L * 1024 * 1024),
+ LARGE(64L * 1024 * 1024);
+
+ private final long sizeInBytes;
+
+ ObjectSize(long sizeInBytes) {
+ this.sizeInBytes = sizeInBytes;
+ }
+
+ public long sizeInBytes() {
+ return sizeInBytes;
+ }
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkHelper.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkHelper.java
new file mode 100644
index 000000000000..354080990673
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkHelper.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark.core;
+
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.EnumMap;
+import java.util.Locale;
+import java.util.Random;
+import software.amazon.awssdk.core.async.AsyncRequestBody;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.S3Object;
+import software.amazon.awssdk.utils.Logger;
+
+/**
+ * Shared S3 operations implementation used by all benchmark classes.
+ */
+public class S3BenchmarkHelper {
+ private static final Logger logger = Logger.loggerFor(S3BenchmarkHelper.class);
+ private static final String TEST_KEY_PREFIX = "benchmark-object-";
+
+ private final S3Wrapper s3Wrapper;
+ private final String name;
+ private String bucketName;
+
+ private final EnumMap testFiles = new EnumMap<>(ObjectSize.class);
+
+ public S3BenchmarkHelper(String name, S3Client s3Client) {
+ this.name = name;
+ this.s3Wrapper = new SyncS3Wrapper(s3Client);
+ }
+
+ public S3BenchmarkHelper(String name, S3AsyncClient s3AsyncClient) {
+ this.name = name;
+ this.s3Wrapper = new AsyncS3Wrapper(s3AsyncClient);
+ }
+
+ public void setup() {
+ try {
+ this.bucketName = name.toLowerCase(Locale.ENGLISH) + "-bucket-" + System.currentTimeMillis();
+ // Create bucket
+ s3Wrapper.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
+
+ // Wait for bucket to be ready
+ s3Wrapper.waitUntilBucketExists(bucketName);
+
+ byte[] testData = new byte[1024 * 1024];
+ new Random().nextBytes(testData);
+
+ for (ObjectSize size : ObjectSize.values()) {
+ Path p = Files.createTempFile(name + "-test-file", null);
+ OutputStream os = Files.newOutputStream(p);
+
+ long chunks = size.sizeInBytes() / testData.length;
+ for (long i = 0; i < chunks; i++) {
+ os.write(testData, 0, testData.length);
+ }
+
+ os.close();
+ testFiles.put(size, p);
+
+ s3Wrapper.putObject(PutObjectRequest.builder().bucket(bucketName).key(objKey(size)).build(), p);
+ }
+ } catch (Exception e) {
+ logger.error(() -> "Setup failed: " + e.getMessage(), e);
+ throw new RuntimeException("Failed to setup S3 benchmark", e);
+ }
+ }
+
+ public String bucketName() {
+ return bucketName;
+ }
+
+ public String objKey(ObjectSize objectSize) {
+ return TEST_KEY_PREFIX + objectSize.name();
+ }
+
+ public RequestBody requestBody(ObjectSize objectSize) {
+ Path p = testFiles.get(objectSize);
+ if (p == null) {
+ throw new RuntimeException("Test file not found: " + objectSize.name());
+ }
+
+ return RequestBody.fromFile(p);
+ }
+
+ public AsyncRequestBody asyncRequestBody(ObjectSize objectSize) {
+ Path p = testFiles.get(objectSize);
+ if (p == null) {
+ throw new RuntimeException("Test file not found: " + objectSize.name());
+ }
+ return AsyncRequestBody.fromFile(p);
+ }
+
+ public void cleanup() {
+ try {
+ // Delete all objects (handle pagination)
+ ListObjectsV2Request.Builder listRequestBuilder = ListObjectsV2Request.builder()
+ .bucket(bucketName);
+ String continuationToken = null;
+ do {
+ if (continuationToken != null) {
+ listRequestBuilder.continuationToken(continuationToken);
+ }
+ ListObjectsV2Response listResponse = s3Wrapper.listObjectsV2(listRequestBuilder.build());
+ for (S3Object object : listResponse.contents()) {
+ s3Wrapper.deleteObject(DeleteObjectRequest.builder()
+ .bucket(bucketName)
+ .key(object.key())
+ .build());
+ }
+ continuationToken = listResponse.nextContinuationToken();
+ } while (continuationToken != null);
+ // Delete bucket
+ s3Wrapper.deleteBucket(DeleteBucketRequest.builder()
+ .bucket(bucketName)
+ .build());
+ logger.info(() -> "Cleaned up bucket: " + bucketName);
+ } catch (Exception e) {
+ logger.warn(() -> "Cleanup failed: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkImpl.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkImpl.java
deleted file mode 100644
index 9eed9e94ad60..000000000000
--- a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3BenchmarkImpl.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.awssdk.benchmark.core;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
-import org.openjdk.jmh.infra.Blackhole;
-import software.amazon.awssdk.core.ResponseInputStream;
-import software.amazon.awssdk.core.sync.RequestBody;
-import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
-import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
-import software.amazon.awssdk.services.s3.model.GetObjectRequest;
-import software.amazon.awssdk.services.s3.model.GetObjectResponse;
-import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
-import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
-import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
-import software.amazon.awssdk.services.s3.model.PutObjectRequest;
-import software.amazon.awssdk.services.s3.model.PutObjectResponse;
-import software.amazon.awssdk.services.s3.model.S3Object;
-import software.amazon.awssdk.utils.Logger;
-
-/**
- * Shared S3 operations implementation used by all benchmark classes.
- */
-public class S3BenchmarkImpl {
- private static final Logger logger = Logger.loggerFor(S3BenchmarkImpl.class);
- private static final String TEST_KEY_PREFIX = "benchmark-object-";
- private static final int OBJECT_COUNT = 100;
-
- private final S3Client s3Client;
- private final String bucketName;
- private final byte[] testData;
-
- public S3BenchmarkImpl(S3Client s3Client, byte[] testData) {
- this.s3Client = s3Client;
- this.bucketName = "benchmark-bucket-" + UUID.randomUUID().toString().substring(0, 8);
- this.testData = testData;
- ThreadLocalRandom.current().nextBytes(testData);
- }
-
- public void setup() {
- try {
- // Create bucket
- s3Client.createBucket(CreateBucketRequest.builder()
- .bucket(bucketName)
- .build());
-
- logger.info(() -> "Created bucket: " + bucketName);
-
- // Wait for bucket to be ready
- s3Client.waiter().waitUntilBucketExists(HeadBucketRequest.builder()
- .bucket(bucketName)
- .build());
-
- // Upload test objects
- for (int i = 0; i < OBJECT_COUNT; i++) {
- String key = TEST_KEY_PREFIX + i;
- s3Client.putObject(
- PutObjectRequest.builder()
- .bucket(bucketName)
- .key(key)
- .build(),
- RequestBody.fromBytes(testData)
- );
- }
-
- logger.info(() -> "Uploaded " + OBJECT_COUNT + " test objects");
-
- } catch (Exception e) {
- logger.error(() -> "Setup failed: " + e.getMessage(), e);
- throw new RuntimeException("Failed to setup S3 benchmark", e);
- }
- }
-
- public void executeGet(String size, Blackhole blackhole) throws IOException {
- // Random key to avoid caching effects
- String key = TEST_KEY_PREFIX + ThreadLocalRandom.current().nextInt(OBJECT_COUNT);
-
- GetObjectRequest request = GetObjectRequest.builder()
- .bucket(bucketName)
- .key(key)
- .build();
-
- ResponseInputStream response = null;
- try {
- response = s3Client.getObject(request);
- byte[] data = readAllBytes(response);
- blackhole.consume(data);
- blackhole.consume(response.response());
- } finally {
- if (response != null) {
- response.close();
- }
- }
- }
-
- public void executePut(String size, Blackhole blackhole) {
- String key = "put-object-" + UUID.randomUUID();
-
- PutObjectRequest request = PutObjectRequest.builder()
- .bucket(bucketName)
- .key(key)
- .build();
-
- PutObjectResponse response = s3Client.putObject(request,
- RequestBody.fromBytes(testData));
-
- blackhole.consume(response);
-
- // Clean up immediately to avoid accumulating objects
- s3Client.deleteObject(DeleteObjectRequest.builder()
- .bucket(bucketName)
- .key(key)
- .build());
- }
-
- public void cleanup() {
- try {
- // Delete all objects (handle pagination)
- ListObjectsV2Request.Builder listRequestBuilder = ListObjectsV2Request.builder()
- .bucket(bucketName);
- String continuationToken = null;
- do {
- if (continuationToken != null) {
- listRequestBuilder.continuationToken(continuationToken);
- }
- ListObjectsV2Response listResponse = s3Client.listObjectsV2(listRequestBuilder.build());
- for (S3Object object : listResponse.contents()) {
- s3Client.deleteObject(DeleteObjectRequest.builder()
- .bucket(bucketName)
- .key(object.key())
- .build());
- }
- continuationToken = listResponse.nextContinuationToken();
- } while (continuationToken != null);
- // Delete bucket
- s3Client.deleteBucket(DeleteBucketRequest.builder()
- .bucket(bucketName)
- .build());
- logger.info(() -> "Cleaned up bucket: " + bucketName);
-
- } catch (Exception e) {
- logger.warn(() -> "Cleanup failed: " + e.getMessage(), e);
- }
- }
-
- private byte[] readAllBytes(InputStream inputStream) throws IOException {
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
- byte[] data = new byte[8192];
- int nRead;
- while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
- buffer.write(data, 0, nRead);
- }
- return buffer.toByteArray();
- }
-}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3Wrapper.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3Wrapper.java
new file mode 100644
index 000000000000..599e93e15094
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/S3Wrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark.core;
+
+import java.nio.file.Path;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+/**
+ * Simple wrapper for S3 APIs so we can implement it using sync or async S3 clients.
+ */
+public interface S3Wrapper {
+ void createBucket(CreateBucketRequest request);
+
+ void putObject(PutObjectRequest request, Path file);
+
+ ListObjectsV2Response listObjectsV2(ListObjectsV2Request request);
+
+ void deleteObject(DeleteObjectRequest request);
+
+ void deleteBucket(DeleteBucketRequest request);
+
+ void waitUntilBucketExists(String bucketName);
+}
diff --git a/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/SyncS3Wrapper.java b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/SyncS3Wrapper.java
new file mode 100644
index 000000000000..50484e5f76d8
--- /dev/null
+++ b/test/http-client-benchmarks/src/main/java/software/amazon/awssdk/benchmark/core/SyncS3Wrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.benchmark.core;
+
+import java.nio.file.Path;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+public class SyncS3Wrapper implements S3Wrapper {
+ private final S3Client s3;
+
+ public SyncS3Wrapper(S3Client s3) {
+ this.s3 = s3;
+ }
+
+ @Override
+ public void createBucket(CreateBucketRequest request) {
+ s3.createBucket(request);
+ }
+
+ @Override
+ public void putObject(PutObjectRequest request, Path file) {
+ s3.putObject(request, RequestBody.fromFile(file));
+ }
+
+ @Override
+ public ListObjectsV2Response listObjectsV2(ListObjectsV2Request request) {
+ return s3.listObjectsV2(request);
+ }
+
+ @Override
+ public void deleteObject(DeleteObjectRequest request) {
+ s3.deleteObject(request);
+ }
+
+ @Override
+ public void deleteBucket(DeleteBucketRequest request) {
+ s3.deleteBucket(request);
+ }
+
+ @Override
+ public void waitUntilBucketExists(String bucketName) {
+ s3.waiter().waitUntilBucketExists(r -> r.bucket(bucketName));
+ }
+}