Skip to content

Commit f72e89d

Browse files
authored
Perf investigations for async clientcore (#41494)
1 parent 72b06ee commit f72e89d

File tree

13 files changed

+433
-25
lines changed

13 files changed

+433
-25
lines changed

common/perf-test-core/src/main/java/com/azure/perf/test/core/ApiPerfTestBase.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.azure.core.http.policy.HttpPipelinePolicy;
1414
import com.azure.core.http.vertx.VertxAsyncHttpClientBuilder;
1515
import com.azure.core.http.vertx.VertxAsyncHttpClientProvider;
16+
import com.azure.core.util.logging.ClientLogger;
1617
import io.netty.handler.ssl.SslContext;
1718
import io.netty.handler.ssl.SslContextBuilder;
1819
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
@@ -23,12 +24,20 @@
2324
import javax.net.ssl.SSLContext;
2425
import javax.net.ssl.SSLException;
2526
import javax.net.ssl.X509TrustManager;
27+
import java.lang.reflect.Method;
2628
import java.net.URI;
2729
import java.security.KeyManagementException;
2830
import java.security.NoSuchAlgorithmException;
2931
import java.security.SecureRandom;
3032
import java.util.Collections;
33+
import java.util.LinkedList;
34+
import java.util.List;
3135
import java.util.concurrent.CompletableFuture;
36+
import java.util.concurrent.ExecutionException;
37+
import java.util.concurrent.ExecutorService;
38+
import java.util.concurrent.Executors;
39+
import java.util.concurrent.Semaphore;
40+
import java.util.concurrent.TimeUnit;
3241

3342
import static com.azure.perf.test.core.PerfStressOptions.HttpClientType.JDK;
3443
import static com.azure.perf.test.core.PerfStressOptions.HttpClientType.NETTY;
@@ -41,6 +50,7 @@
4150
* @param <TOptions> the performance test options to use while running the test.
4251
*/
4352
public abstract class ApiPerfTestBase<TOptions extends PerfStressOptions> extends PerfTestBase<TOptions> {
53+
ClientLogger LOGGER = new ClientLogger(ApiPerfTestBase.class);
4454
private final reactor.netty.http.client.HttpClient recordPlaybackHttpClient;
4555
private final URI testProxy;
4656
private final TestProxyPolicy testProxyPolicy;
@@ -225,6 +235,106 @@ public Mono<Void> runAllAsync(long endNanoTime) {
225235
.then();
226236
}
227237

238+
public CompletableFuture<Void> runAllAsyncWithCompletableFuture(long endNanoTime) {
239+
completedOperations = 0;
240+
lastCompletionNanoTime = 0;
241+
long startNanoTime = System.nanoTime();
242+
Semaphore semaphore = new Semaphore(options.getParallel()); // Use configurable limit
243+
244+
List<CompletableFuture<Void>> futures = new LinkedList<>();
245+
while (System.nanoTime() < endNanoTime) {
246+
try {
247+
semaphore.acquire();
248+
// Each runTestAsyncWithCompletableFuture() call runs independently
249+
CompletableFuture<Void> testFuture = runTestAsyncWithCompletableFuture()
250+
.thenAccept(result -> {
251+
completedOperations += result;
252+
lastCompletionNanoTime = System.nanoTime() - startNanoTime;
253+
})
254+
.whenComplete((res, ex) -> semaphore.release());
255+
futures.add(testFuture);
256+
} catch (InterruptedException e) {
257+
Thread.currentThread().interrupt();
258+
throw new RuntimeException(e);
259+
}
260+
}
261+
262+
// Remove all completed CompletableFutures from the list
263+
futures.removeIf(CompletableFuture::isDone);
264+
// Combine all futures so we can wait for all to complete
265+
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
266+
}
267+
268+
@Override
269+
public Runnable runAllAsyncWithExecutorService(long endNanoTime) {
270+
completedOperations = 0;
271+
lastCompletionNanoTime = 0;
272+
final ExecutorService executor = Executors.newFixedThreadPool(options.getParallel());
273+
274+
return () -> {
275+
try {
276+
while (System.nanoTime() < endNanoTime) {
277+
long startNanoTime = System.nanoTime();
278+
279+
try {
280+
Runnable task = runTestAsyncWithExecutorService();
281+
executor.submit(() -> {
282+
task.run();
283+
completedOperations++;
284+
lastCompletionNanoTime = System.nanoTime() - startNanoTime;
285+
}).get(); // Wait for the task to complete
286+
} catch (InterruptedException | ExecutionException e) {
287+
e.printStackTrace();
288+
}
289+
}
290+
} finally {
291+
executor.shutdown();
292+
try {
293+
if (!executor.awaitTermination(options.getDuration(), TimeUnit.SECONDS)) {
294+
executor.shutdownNow();
295+
}
296+
} catch (InterruptedException e) {
297+
executor.shutdownNow();
298+
Thread.currentThread().interrupt();
299+
}
300+
}
301+
};
302+
}
303+
304+
@Override
305+
public Runnable runAllAsyncWithVirtualThread(long endNanoTime) {
306+
completedOperations = 0;
307+
lastCompletionNanoTime = 0;
308+
309+
ExecutorService virtualThreadExecutor;
310+
try {
311+
Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
312+
virtualThreadExecutor = (ExecutorService) method.invoke(null);
313+
} catch (Exception e) {
314+
// Skip virtual thread tests and report 0 completed operations rather than fallback
315+
return () -> {
316+
completedOperations = 0;
317+
lastCompletionNanoTime = 0;
318+
};
319+
}
320+
321+
return () -> {
322+
while (System.nanoTime() < endNanoTime) {
323+
long startNanoTime = System.nanoTime();
324+
virtualThreadExecutor.execute(() -> {
325+
try {
326+
runTestAsyncWithVirtualThread();
327+
completedOperations++;
328+
lastCompletionNanoTime = System.nanoTime() - startNanoTime;
329+
} catch (Exception e) {
330+
LOGGER.logThrowableAsError(e);
331+
}
332+
});
333+
}
334+
virtualThreadExecutor.shutdown();
335+
};
336+
}
337+
228338
/**
229339
* Indicates how many operations were completed in a single run of the test. Good to be used for batch operations.
230340
*
@@ -240,6 +350,31 @@ public Mono<Void> runAllAsync(long endNanoTime) {
240350
*/
241351
abstract Mono<Integer> runTestAsync();
242352

353+
/**
354+
* Indicates how many operations were completed in a single run of the async test using CompletableFuture.
355+
*
356+
* @return the number of successful operations completed.
357+
*/
358+
CompletableFuture<Integer> runTestAsyncWithCompletableFuture() {
359+
throw new UnsupportedOperationException("runAllAsyncWithCompletableFuture is not supported.");
360+
}
361+
362+
/**
363+
* Indicates how many operations were completed in a single run of the async test using ExecutorService.
364+
*
365+
* @return the number of successful operations completed.
366+
*/
367+
Runnable runTestAsyncWithExecutorService() {
368+
throw new UnsupportedOperationException("runAllAsyncWithExecutorService is not supported.");
369+
}
370+
371+
/**
372+
* Indicates how many operations were completed in a single run of the async test using Virtual Threads.
373+
*/
374+
Runnable runTestAsyncWithVirtualThread() {
375+
throw new UnsupportedOperationException("runAllAsyncWithVirtualThread is not supported.");
376+
}
377+
243378
/**
244379
* Stops playback tests.
245380
*
@@ -327,6 +462,12 @@ private Mono<Void> runSyncOrAsync() {
327462
return Mono.defer(() -> {
328463
if (options.isSync()) {
329464
return Mono.fromFuture(CompletableFuture.supplyAsync(() -> runTest())).then();
465+
} else if (options.isCompletableFuture()) {
466+
return Mono.fromFuture(CompletableFuture.supplyAsync(() -> runTestAsyncWithCompletableFuture())).then();
467+
} else if (options.isExecutorService()) {
468+
return Mono.fromRunnable(runTestAsyncWithExecutorService());
469+
} else if (options.isVirtualThread()) {
470+
return Mono.fromRunnable(this::runTestAsyncWithVirtualThread);
330471
} else {
331472
return runTestAsync().then();
332473
}

common/perf-test-core/src/main/java/com/azure/perf/test/core/PerfStressOptions.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
2-
// Licensed under the MIT License.
3-
41
package com.azure.perf.test.core;
52

63
import com.azure.core.util.ExpandableStringEnum;
@@ -49,6 +46,15 @@ public class PerfStressOptions {
4946
@Parameter(names = { "--http-client" }, description = "The http client to use. Can be netty, okhttp, jdk, vertx or a full name of HttpClientProvider implementation class.")
5047
private String httpClient = HttpClientType.NETTY.toString();
5148

49+
@Parameter(names = { "--completeablefuture" }, help = true, description = "Runs the performance test asynchronously as a CompletableFuture.")
50+
private boolean completeablefuture = false;
51+
52+
@Parameter(names = { "--executorservice" }, help = true, description = "Runs the performance test asynchronously with an ExecutorService.")
53+
private boolean executorservice = false;
54+
55+
@Parameter(names = { "--virtualthread" }, help = true, description = "Runs the performance test asynchronously with a virtual thread.")
56+
private boolean virtualthread = false;
57+
5258
/**
5359
* Get the configured count for performance test.
5460
* @return The count.
@@ -129,6 +135,30 @@ public boolean isSync() {
129135
return sync;
130136
}
131137

138+
/**
139+
* Get the configured CompletableFuture status for performance test.
140+
* @return The CompletableFuture status.
141+
*/
142+
public boolean isCompletableFuture() {
143+
return completeablefuture;
144+
}
145+
146+
/**
147+
* Get the configured ExecutorService status for performance test.
148+
* @return The ExecutorService status.
149+
*/
150+
public boolean isExecutorService() {
151+
return executorservice;
152+
}
153+
154+
/**
155+
* Get the configured VirtualThread status for performance test.
156+
* @return The VirtualThread status.
157+
*/
158+
public boolean isVirtualThread() {
159+
return virtualthread;
160+
}
161+
132162
/**
133163
* The http client to use. Can be netty, okhttp.
134164
* @return The http client to use.

common/perf-test-core/src/main/java/com/azure/perf/test/core/PerfStressProgram.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
import java.lang.reflect.Method;
1212
import java.util.ArrayList;
1313
import java.util.Arrays;
14+
import java.util.LinkedList;
1415
import java.util.List;
1516
import java.util.Map;
1617
import java.util.Timer;
1718
import java.util.TimerTask;
1819
import java.util.TreeMap;
1920
import java.util.concurrent.Callable;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.ExecutorService;
23+
import java.util.concurrent.Executors;
2024
import java.util.concurrent.ForkJoinPool;
2125
import java.util.concurrent.TimeUnit;
2226
import java.util.concurrent.atomic.AtomicBoolean;
@@ -156,15 +160,19 @@ public static void run(Class<?> testClass, PerfStressOptions options) {
156160
}
157161

158162
if (options.getWarmup() > 0) {
159-
runTests(tests, options.isSync(), options.getParallel(), options.getWarmup(), "Warmup");
163+
runTests(tests, options.isSync(), options.isCompletableFuture(), options.isExecutorService(),
164+
options.isVirtualThread(),
165+
options.getParallel(),
166+
options.getWarmup(), "Warmup");
160167
}
161168

162169
for (int i = 0; i < options.getIterations(); i++) {
163170
String title = "Test";
164171
if (options.getIterations() > 1) {
165172
title += " " + (i + 1);
166173
}
167-
runTests(tests, options.isSync(), options.getParallel(), options.getDuration(), title);
174+
runTests(tests, options.isSync(), options.isCompletableFuture(), options.isExecutorService(),
175+
startedPlayback, options.getParallel(), options.getDuration(), title);
168176
}
169177
} finally {
170178
try {
@@ -253,13 +261,19 @@ private static void writeKeyValue(String key, Object value, StringBuilder sb, At
253261
*
254262
* @param tests the performance tests to be executed.
255263
* @param sync indicate if synchronous test should be run.
264+
* @param completableFuture indicate if completable future test should be run.
265+
* @param executorService indicate if executor service test should be run.
266+
* @param virtualThread indicate if virtual thread test should be run.
256267
* @param parallel the number of parallel threads to run the performance test on.
257268
* @param durationSeconds the duration for which performance test should be run on.
258269
* @param title the title of the performance tests.
270+
*
259271
* @throws RuntimeException if the execution fails.
260272
* @throws IllegalStateException if zero operations completed of the performance test.
261273
*/
262-
public static void runTests(PerfTestBase<?>[] tests, boolean sync, int parallel, int durationSeconds, String title) {
274+
public static void runTests(PerfTestBase<?>[] tests, boolean sync, boolean completableFuture,
275+
boolean executorService, boolean virtualThread, int parallel, int durationSeconds,
276+
String title) {
263277

264278
long endNanoTime = System.nanoTime() + ((long) durationSeconds * 1000000000);
265279

@@ -288,6 +302,37 @@ public static void runTests(PerfTestBase<?>[] tests, boolean sync, int parallel,
288302
forkJoinPool.invokeAll(operations);
289303

290304
forkJoinPool.awaitQuiescence(durationSeconds + 1, TimeUnit.SECONDS);
305+
} else if (completableFuture) {
306+
List<CompletableFuture<Void>> futures = new LinkedList<>();
307+
for (PerfTestBase<?> test : tests) {
308+
futures.add(test.runAllAsyncWithCompletableFuture(endNanoTime));
309+
}
310+
CompletableFuture<Void> allFutures =
311+
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
312+
allFutures.get(); // Wait for all futures to complete
313+
} else if (executorService) {
314+
// when updated to concurrentTaskLimit, the performance drops?
315+
ExecutorService executor = Executors.newFixedThreadPool(tests.length);
316+
try {
317+
for (PerfTestBase<?> test : tests) {
318+
Runnable task = test.runAllAsyncWithExecutorService(endNanoTime);
319+
executor.submit(task);
320+
}
321+
} finally {
322+
executor.shutdown();
323+
try {
324+
if (!executor.awaitTermination(durationSeconds + 1, TimeUnit.SECONDS)) {
325+
executor.shutdownNow();
326+
}
327+
} catch (InterruptedException e) {
328+
executor.shutdownNow();
329+
Thread.currentThread().interrupt();
330+
}
331+
}
332+
} else if (virtualThread) {
333+
for (PerfTestBase<?> test : tests) {
334+
test.runAllAsyncWithVirtualThread(endNanoTime);
335+
}
291336
} else {
292337
// Exceptions like OutOfMemoryError are handled differently by the default Reactor schedulers. Instead of terminating the
293338
// Flux, the Flux will hang and the exception is only sent to the thread's uncaughtExceptionHandler and the Reactor

common/perf-test-core/src/main/java/com/azure/perf/test/core/PerfStressTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package com.azure.perf.test.core;
55

6+
import java.util.concurrent.CompletableFuture;
67
import reactor.core.publisher.Mono;
78

89
/**
@@ -37,6 +38,21 @@ Mono<Integer> runTestAsync() {
3738
return runAsync().then(Mono.just(1));
3839
}
3940

41+
@Override
42+
CompletableFuture<Integer> runTestAsyncWithCompletableFuture() {
43+
return runAsyncWithCompletableFuture().thenApply(unused -> 1);
44+
}
45+
46+
@Override
47+
Runnable runTestAsyncWithExecutorService() {
48+
return runAsyncWithExecutorService();
49+
}
50+
51+
@Override
52+
Runnable runTestAsyncWithVirtualThread() {
53+
return runAsyncWithVirtualThread();
54+
}
55+
4056
/**
4157
* Runs the performance test.
4258
*/
@@ -47,4 +63,27 @@ Mono<Integer> runTestAsync() {
4763
* @return An empty {@link Mono}
4864
*/
4965
public abstract Mono<Void> runAsync();
66+
67+
/**
68+
* Runs the performance test asynchronously.
69+
* @return An empty {@link CompletableFuture}
70+
*/
71+
public CompletableFuture<Void> runAsyncWithCompletableFuture() {
72+
throw new UnsupportedOperationException("runAsyncWithCompletableFuture is not supported.");
73+
}
74+
75+
/**
76+
* Runs the performance test asynchronously.
77+
* @return An empty {@link Runnable}
78+
*/
79+
public Runnable runAsyncWithExecutorService() {
80+
throw new UnsupportedOperationException("runAsyncWithExecutorService is not supported.");
81+
}
82+
83+
/**
84+
* Runs the performance test asynchronously.
85+
*/
86+
public Runnable runAsyncWithVirtualThread() {
87+
throw new UnsupportedOperationException("runAsyncWithVirtualThread is not supported.");
88+
}
5089
}

0 commit comments

Comments
 (0)