From 702ae2eea5e64aa3f556771bf90935f188f01739 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 13 Nov 2024 21:44:32 +1000 Subject: [PATCH] initial tests for regular performance testing --- .../performance/ComplexQueryPerformance.java | 272 ++++++++++++++++++ ...OverlappingFieldValidationPerformance.java | 86 ++++++ .../performance/PerformanceTestingUtils.java | 84 ++++++ 3 files changed, 442 insertions(+) create mode 100644 src/test/java/performance/ComplexQueryPerformance.java create mode 100644 src/test/java/performance/OverlappingFieldValidationPerformance.java create mode 100644 src/test/java/performance/PerformanceTestingUtils.java diff --git a/src/test/java/performance/ComplexQueryPerformance.java b/src/test/java/performance/ComplexQueryPerformance.java new file mode 100644 index 000000000..e8b2c3da0 --- /dev/null +++ b/src/test/java/performance/ComplexQueryPerformance.java @@ -0,0 +1,272 @@ +package performance; + +import benchmark.BenchmarkUtils; +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. + *

+ * 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 ComplexQueryPerformance { + + @Param({"5", "10", "20"}) + int howManyItems = 5; + int howLongToSleep = 5; + int howManyQueries = 10; + 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() { + + ComplexQueryPerformance complexQueryBenchmark = new ComplexQueryPerformance(); + complexQueryBenchmark.howManyQueries = 5; + complexQueryBenchmark.howManyItems = 10; + + BenchmarkUtils.runInToolingForSomeTimeThenExit( + complexQueryBenchmark::setUp, + complexQueryBenchmark::runManyQueriesToCompletion, + complexQueryBenchmark::tearDown + + ); + } + + + @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 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(PerformanceTestingUtils.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 CompletableFuture supplyAsyncListItems(DataFetchingEnvironment environment, Supplier codeToRun) { + return supplyAsync(codeToRun); + } + + private CompletableFuture supplyAsync(Supplier 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 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 mkHowManyThings(Integer howMany) { + ImmutableList.Builder 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; + } + } +} diff --git a/src/test/java/performance/OverlappingFieldValidationPerformance.java b/src/test/java/performance/OverlappingFieldValidationPerformance.java new file mode 100644 index 000000000..77a2af6ae --- /dev/null +++ b/src/test/java/performance/OverlappingFieldValidationPerformance.java @@ -0,0 +1,86 @@ +package performance; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.i18n.I18n; +import graphql.language.Document; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaGenerator; +import graphql.validation.LanguageTraversal; +import graphql.validation.RulesVisitor; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationError; +import graphql.validation.ValidationErrorCollector; +import graphql.validation.rules.OverlappingFieldsCanBeMerged; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static graphql.Assert.assertTrue; + +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) +public class OverlappingFieldValidationPerformance { + + @State(Scope.Benchmark) + public static class MyState { + + GraphQLSchema schema; + Document document; + + @Setup + public void setup() { + try { + String schemaString = PerformanceTestingUtils.loadResource("large-schema-4.graphqls"); + String query = PerformanceTestingUtils.loadResource("large-schema-4-query.graphql"); + schema = SchemaGenerator.createdMockedSchema(schemaString); + document = Parser.parse(query); + + // make sure this is a valid query overall + GraphQL graphQL = GraphQL.newGraphQL(schema).build(); + ExecutionResult executionResult = graphQL.execute(query); + assertTrue(executionResult.getErrors().size() == 0); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void overlappingFieldValidationAbgTime(MyState myState, Blackhole blackhole) { + blackhole.consume(validateQuery(myState.schema, myState.document)); + } + + @Benchmark + @OutputTimeUnit(TimeUnit.SECONDS) + public void overlappingFieldValidationThroughput(MyState myState, Blackhole blackhole) { + blackhole.consume(validateQuery(myState.schema, myState.document)); + } + + private List validateQuery(GraphQLSchema schema, Document document) { + ValidationErrorCollector errorCollector = new ValidationErrorCollector(); + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH); + ValidationContext validationContext = new ValidationContext(schema, document, i18n); + OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector); + LanguageTraversal languageTraversal = new LanguageTraversal(); + languageTraversal.traverse(document, new RulesVisitor(validationContext, Collections.singletonList(overlappingFieldsCanBeMerged))); + return errorCollector.getErrors(); + } +} diff --git a/src/test/java/performance/PerformanceTestingUtils.java b/src/test/java/performance/PerformanceTestingUtils.java new file mode 100644 index 000000000..9e05fd661 --- /dev/null +++ b/src/test/java/performance/PerformanceTestingUtils.java @@ -0,0 +1,84 @@ +package performance; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.Callable; + +public class PerformanceTestingUtils { + + @SuppressWarnings("UnstableApiUsage") + static String loadResource(String name) { + return asRTE(() -> { + URL resource = PerformanceTestingUtils.class.getClassLoader().getResource(name); + if (resource == null) { + throw new IllegalArgumentException("missing resource: " + name); + } + byte[] bytes; + try (InputStream inputStream = resource.openStream()) { + bytes = inputStream.readAllBytes(); + } + return new String(bytes, Charset.defaultCharset()); + }); + } + + static T asRTE(Callable callable) { + try { + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void runInToolingForSomeTimeThenExit(Runnable setup, Runnable r, Runnable tearDown) { + int runForMillis = getRunForMillis(); + if (runForMillis <= 0) { + System.out.print("'runForMillis' environment var is not set - continuing \n"); + return; + } + System.out.printf("Running initial code in some tooling - runForMillis=%d \n", runForMillis); + System.out.print("Get your tooling in order and press enter..."); + readLine(); + System.out.print("Lets go...\n"); + setup.run(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss"); + long now, then = System.currentTimeMillis(); + do { + now = System.currentTimeMillis(); + long msLeft = runForMillis - (now - then); + System.out.printf("\t%s Running in loop... %s ms left\n", dtf.format(LocalDateTime.now()), msLeft); + r.run(); + now = System.currentTimeMillis(); + } while ((now - then) < runForMillis); + + tearDown.run(); + + System.out.printf("This ran for %d millis. Exiting...\n", System.currentTimeMillis() - then); + System.exit(0); + } + + private static void readLine() { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + try { + br.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static int getRunForMillis() { + String runFor = System.getenv("runForMillis"); + try { + return Integer.parseInt(runFor); + } catch (NumberFormatException e) { + return -1; + } + } + +}