-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3476 from graphql-java/cf-based-app-benchmark
Added a more complex query benchmark
- Loading branch information
Showing
2 changed files
with
310 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
package benchmark; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import graphql.ExecutionInput; | ||
import graphql.ExecutionResult; | ||
import graphql.GraphQL; | ||
import graphql.schema.DataFetcher; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import graphql.schema.GraphQLSchema; | ||
import graphql.schema.idl.RuntimeWiring; | ||
import graphql.schema.idl.SchemaGenerator; | ||
import graphql.schema.idl.SchemaParser; | ||
import graphql.schema.idl.TypeDefinitionRegistry; | ||
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.profile.GCProfiler; | ||
import org.openjdk.jmh.runner.Runner; | ||
import org.openjdk.jmh.runner.options.Options; | ||
import org.openjdk.jmh.runner.options.OptionsBuilder; | ||
|
||
import java.util.List; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import java.util.function.Supplier; | ||
|
||
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; | ||
|
||
/** | ||
* This benchmark is an attempt to have a more complex query that involves async and sync work together | ||
* along with multiple threads happening. | ||
* <p> | ||
* It can also be run in a forever mode say if you want to connect a profiler to it say | ||
*/ | ||
@State(Scope.Benchmark) | ||
@Warmup(iterations = 2, time = 5) | ||
@Measurement(iterations = 2) | ||
@Fork(2) | ||
public class ComplexQueryBenchmark { | ||
|
||
@Param({"5", "20", "100"}) | ||
int howManyItems = 5; | ||
int howLongToSleep = 5; | ||
int howManyQueries = 50; | ||
int howManyQueryThreads = 10; | ||
int howManyFetcherThreads = 10; | ||
|
||
ExecutorService queryExecutorService; | ||
ExecutorService fetchersExecutorService; | ||
GraphQL graphQL; | ||
volatile boolean shutDown; | ||
|
||
@Setup(Level.Trial) | ||
public void setUp() { | ||
shutDown = false; | ||
queryExecutorService = Executors.newFixedThreadPool(howManyQueryThreads); | ||
fetchersExecutorService = Executors.newFixedThreadPool(howManyFetcherThreads); | ||
graphQL = buildGraphQL(); | ||
} | ||
|
||
@TearDown(Level.Trial) | ||
public void tearDown() { | ||
shutDown = true; | ||
queryExecutorService.shutdownNow(); | ||
fetchersExecutorService.shutdownNow(); | ||
} | ||
|
||
|
||
@Benchmark | ||
@BenchmarkMode(Mode.Throughput) | ||
@OutputTimeUnit(TimeUnit.SECONDS) | ||
public Object benchMarkSimpleQueriesThroughput() { | ||
return runManyQueriesToCompletion(); | ||
} | ||
|
||
|
||
public static void main(String[] args) throws Exception { | ||
// just to make sure it's all valid before testing | ||
runAtStartup(); | ||
|
||
Options opt = new OptionsBuilder() | ||
.include("benchmark.ComplexQueryBenchmark") | ||
.addProfiler(GCProfiler.class) | ||
.build(); | ||
|
||
new Runner(opt).run(); | ||
} | ||
|
||
@SuppressWarnings({"ConstantValue", "LoopConditionNotUpdatedInsideLoop"}) | ||
private static void runAtStartup() { | ||
// set this to true if you want to hook in profiler say to a forever running JVM | ||
boolean forever = Boolean.getBoolean("forever"); | ||
|
||
long then = System.currentTimeMillis(); | ||
System.out.printf("Running initial code before starting the benchmark in forever=%b mode \n", forever); | ||
ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); | ||
complexQueryBenchmark.setUp(); | ||
do { | ||
System.out.print("Running queries....\n"); | ||
complexQueryBenchmark.howManyItems = 100; | ||
complexQueryBenchmark.runManyQueriesToCompletion(); | ||
} while (forever); | ||
complexQueryBenchmark.tearDown(); | ||
|
||
System.out.printf("This took %d millis\n", System.currentTimeMillis() - then); | ||
|
||
} | ||
|
||
@SuppressWarnings("UnnecessaryLocalVariable") | ||
private Void runManyQueriesToCompletion() { | ||
CompletableFuture<?>[] cfs = new CompletableFuture[howManyQueries]; | ||
for (int i = 0; i < howManyQueries; i++) { | ||
cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), queryExecutorService).thenCompose(cf -> cf); | ||
} | ||
Void result = CompletableFuture.allOf(cfs).join(); | ||
return result; | ||
} | ||
|
||
public CompletableFuture<ExecutionResult> executeQuery(int howMany, int howLong) { | ||
String fields = "id name f1 f2 f3 f4 f5 f6 f7 f8 f9 f10"; | ||
String query = "query q {" | ||
+ String.format("shops(howMany : %d) { %s departments( howMany : %d) { %s products(howMany : %d) { %s }}}\n" | ||
, howMany, fields, 10, fields, 5, fields) | ||
+ String.format("expensiveShops(howMany : %d howLong : %d) { %s expensiveDepartments( howMany : %d howLong : %d) { %s expensiveProducts(howMany : %d howLong : %d) { %s }}}\n" | ||
, howMany, howLong, fields, 10, howLong, fields, 5, howLong, fields) | ||
+ "}"; | ||
return graphQL.executeAsync(ExecutionInput.newExecutionInput(query).build()); | ||
} | ||
|
||
private GraphQL buildGraphQL() { | ||
TypeDefinitionRegistry definitionRegistry = new SchemaParser().parse(BenchmarkUtils.loadResource("storesanddepartments.graphqls")); | ||
|
||
DataFetcher<?> shopsDF = env -> mkHowManyThings(env.getArgument("howMany")); | ||
DataFetcher<?> expensiveShopsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); | ||
DataFetcher<?> departmentsDF = env -> mkHowManyThings(env.getArgument("howMany")); | ||
DataFetcher<?> expensiveDepartmentsDF = env -> supplyAsyncListItems(env, () -> sleepAndReturnThings(env)); | ||
DataFetcher<?> productsDF = env -> mkHowManyThings(env.getArgument("howMany")); | ||
DataFetcher<?> expensiveProductsDF = env -> supplyAsyncListItems(env, () -> sleepAndReturnThings(env)); | ||
|
||
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() | ||
.type(newTypeWiring("Query") | ||
.dataFetcher("shops", shopsDF) | ||
.dataFetcher("expensiveShops", expensiveShopsDF)) | ||
.type(newTypeWiring("Shop") | ||
.dataFetcher("departments", departmentsDF) | ||
.dataFetcher("expensiveDepartments", expensiveDepartmentsDF)) | ||
.type(newTypeWiring("Department") | ||
.dataFetcher("products", productsDF) | ||
.dataFetcher("expensiveProducts", expensiveProductsDF)) | ||
.build(); | ||
|
||
GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(definitionRegistry, runtimeWiring); | ||
|
||
return GraphQL.newGraphQL(graphQLSchema).build(); | ||
} | ||
|
||
private <T> CompletableFuture<T> supplyAsyncListItems(DataFetchingEnvironment environment, Supplier<T> codeToRun) { | ||
return supplyAsync(codeToRun); | ||
} | ||
|
||
private <T> CompletableFuture<T> supplyAsync(Supplier<T> codeToRun) { | ||
if (!shutDown) { | ||
//logEvery(100, "async fetcher"); | ||
return CompletableFuture.supplyAsync(codeToRun, fetchersExecutorService); | ||
} else { | ||
// if we have shutdown - get on with it, so we shut down quicker | ||
return CompletableFuture.completedFuture(codeToRun.get()); | ||
} | ||
} | ||
|
||
private List<IdAndNamedThing> sleepAndReturnThings(DataFetchingEnvironment env) { | ||
// by sleeping, we hope to cause the objects to stay longer in GC land and hence have a longer lifecycle | ||
// then a simple stack say or young gen gc. I don't know this will work, but I am trying it | ||
// to represent work that takes some tie to complete | ||
sleep(env.getArgument("howLong")); | ||
return mkHowManyThings(env.getArgument("howMany")); | ||
} | ||
|
||
private void sleep(Integer howLong) { | ||
if (howLong > 0) { | ||
try { | ||
Thread.sleep(howLong); | ||
} catch (InterruptedException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} | ||
|
||
AtomicInteger logCount = new AtomicInteger(); | ||
private void logEvery(int every, String s) { | ||
int count = logCount.getAndIncrement(); | ||
if (count == 0 || count % every == 0) { | ||
System.out.println("\t" + count + "\t" + s); | ||
} | ||
} | ||
|
||
private List<IdAndNamedThing> mkHowManyThings(Integer howMany) { | ||
ImmutableList.Builder<IdAndNamedThing> builder = ImmutableList.builder(); | ||
for (int i = 0; i < howMany; i++) { | ||
builder.add(new IdAndNamedThing(i)); | ||
} | ||
return builder.build(); | ||
} | ||
|
||
@SuppressWarnings("unused") | ||
static class IdAndNamedThing { | ||
private final int i; | ||
|
||
public IdAndNamedThing(int i) { | ||
this.i = i; | ||
} | ||
|
||
public String getId() { | ||
return "id" + i; | ||
} | ||
|
||
public String getName() { | ||
return "name" + i; | ||
} | ||
|
||
public String getF1() { | ||
return "f1" + i; | ||
} | ||
|
||
public String getF2() { | ||
return "f2" + i; | ||
} | ||
|
||
public String getF3() { | ||
return "f3" + i; | ||
} | ||
|
||
public String getF4() { | ||
return "f4" + i; | ||
} | ||
|
||
public String getF5() { | ||
return "f5" + i; | ||
} | ||
|
||
public String getF6() { | ||
return "f6" + i; | ||
} | ||
|
||
public String getF7() { | ||
return "f7" + i; | ||
} | ||
|
||
public String getF8() { | ||
return "f8" + i; | ||
} | ||
|
||
public String getF9() { | ||
return "f9" + i; | ||
} | ||
|
||
public String getF10() { | ||
return "f10" + i; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters