diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 59efc4f48d..8e8c31b6e6 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -32,6 +32,7 @@ public class ExecutionInput { // this is currently not used but we want it back soon after the v23 release private final AtomicBoolean cancelled; + private final boolean profileExecution; @Internal private ExecutionInput(Builder builder) { @@ -47,6 +48,7 @@ private ExecutionInput(Builder builder) { this.localContext = builder.localContext; this.extensions = builder.extensions; this.cancelled = builder.cancelled; + this.profileExecution = builder.profileExecution; } /** @@ -142,6 +144,10 @@ public Map getExtensions() { return extensions; } + public boolean isProfileExecution() { + return profileExecution; + } + /** * This helps you transform the current ExecutionInput object into another one by starting a builder with all * the current values and allows you to transform it how you want. @@ -163,7 +169,9 @@ public ExecutionInput transform(Consumer builderConsumer) { .variables(this.rawVariables.toMap()) .extensions(this.extensions) .executionId(this.executionId) - .locale(this.locale); + .locale(this.locale) + .profileExecution(this.profileExecution); + builderConsumer.accept(builder); @@ -221,6 +229,7 @@ public static class Builder { private Locale locale = Locale.getDefault(); private ExecutionId executionId; private AtomicBoolean cancelled = new AtomicBoolean(false); + private boolean profileExecution; public Builder query(String query) { this.query = assertNotNull(query, () -> "query can't be null"); @@ -360,6 +369,11 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { return this; } + public Builder profileExecution(boolean profileExecution) { + this.profileExecution = profileExecution; + return this; + } + public ExecutionInput build() { return new ExecutionInput(this); } diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 4443ed9f76..6c35c93493 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -412,6 +412,9 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { + Profiler profiler = executionInput.isProfileExecution() ? new ProfilerImpl() : Profiler.NO_OP; + profiler.start(); + ExecutionInput executionInputWithId = ensureInputHasId(executionInput); CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInputWithId)); @@ -426,7 +429,7 @@ public CompletableFuture executeAsync(ExecutionInput executionI GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters, instrumentationState); - CompletableFuture executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState); + CompletableFuture executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState, profiler); // // finish up instrumentation executionResult = executionResult.whenComplete(completeInstrumentationCtxCF(executionInstrumentation)); @@ -460,12 +463,12 @@ private ExecutionInput ensureInputHasId(ExecutionInput executionInput) { } - private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { + private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, Profiler profiler) { AtomicReference executionInputRef = new AtomicReference<>(executionInput); Function computeFunction = transformedInput -> { // if they change the original query in the pre-parser, then we want to see it downstream from then on executionInputRef.set(transformedInput); - return parseAndValidate(executionInputRef, graphQLSchema, instrumentationState); + return parseAndValidate(executionInputRef, graphQLSchema, instrumentationState, profiler); }; CompletableFuture preparsedDoc = preparsedDocumentProvider.getDocumentAsync(executionInput, computeFunction); return preparsedDoc.thenCompose(preparsedDocumentEntry -> { @@ -473,14 +476,14 @@ private CompletableFuture parseValidateAndExecute(ExecutionInpu return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDocumentEntry.getErrors())); } try { - return execute(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState); + return executeImpl(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState, profiler); } catch (AbortExecutionException e) { return CompletableFuture.completedFuture(e.toExecutionResult()); } }); } - private PreparsedDocumentEntry parseAndValidate(AtomicReference executionInputRef, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { + private PreparsedDocumentEntry parseAndValidate(AtomicReference executionInputRef, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, Profiler profiler) { ExecutionInput executionInput = executionInputRef.get(); @@ -533,13 +536,14 @@ private List validate(ExecutionInput executionInput, Document d return validationErrors; } - private CompletableFuture execute(ExecutionInput executionInput, - Document document, - GraphQLSchema graphQLSchema, - InstrumentationState instrumentationState + private CompletableFuture executeImpl(ExecutionInput executionInput, + Document document, + GraphQLSchema graphQLSchema, + InstrumentationState instrumentationState, + Profiler profiler ) { - Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader); + Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader, profiler); ExecutionId executionId = executionInput.getExecutionId(); return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState); diff --git a/src/main/java/graphql/Profiler.java b/src/main/java/graphql/Profiler.java new file mode 100644 index 0000000000..066cff6cf2 --- /dev/null +++ b/src/main/java/graphql/Profiler.java @@ -0,0 +1,29 @@ +package graphql; + +import graphql.schema.DataFetcher; +import org.jspecify.annotations.NullMarked; + +@Internal +@NullMarked +public interface Profiler { + + Profiler NO_OP = new Profiler() { + }; + + default void start() { + + } + + + default void rootFieldCount(int size) { + + } + + default void subSelectionCount(int size) { + + } + + default void fieldFetched(Object fetchedObject, DataFetcher dataFetcher) { + + } +} diff --git a/src/main/java/graphql/ProfilerImpl.java b/src/main/java/graphql/ProfilerImpl.java new file mode 100644 index 0000000000..8d6cc78c18 --- /dev/null +++ b/src/main/java/graphql/ProfilerImpl.java @@ -0,0 +1,37 @@ +package graphql; + +import graphql.schema.DataFetcher; +import graphql.schema.PropertyDataFetcher; +import org.jspecify.annotations.NullMarked; + +import java.util.concurrent.atomic.AtomicInteger; + +@Internal +@NullMarked +public class ProfilerImpl implements Profiler { + + volatile long startTime; + volatile int rootFieldCount; + + AtomicInteger fieldCount; + AtomicInteger propertyDataFetcherCount; + + @Override + public void start() { + startTime = System.nanoTime(); + } + + + @Override + public void rootFieldCount(int count) { + this.rootFieldCount = count; + } + + @Override + public void fieldFetched(Object fetchedObject, DataFetcher dataFetcher) { + fieldCount.incrementAndGet(); + if (dataFetcher instanceof PropertyDataFetcher) { + propertyDataFetcherCount.incrementAndGet(); + } + } +} diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 138e4fb973..eaf1d21736 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -62,6 +62,7 @@ public CompletableFuture execute(ExecutionContext executionCont futures.await().whenComplete((completeValueInfos, throwable) -> { executionContext.run(throwable,() -> { + executionContext.getProfiler().rootFieldCount(completeValueInfos.size()); List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldsExecutedOnInitialResult, overallResult); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 0373d6564c..00cde0085c 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -9,6 +9,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.Profiler; import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -57,19 +58,22 @@ public class Execution { private final Instrumentation instrumentation; private final ValueUnboxer valueUnboxer; private final boolean doNotAutomaticallyDispatchDataLoader; + private final Profiler profiler; public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, Instrumentation instrumentation, ValueUnboxer valueUnboxer, - boolean doNotAutomaticallyDispatchDataLoader) { + boolean doNotAutomaticallyDispatchDataLoader, + Profiler profiler) { this.queryStrategy = queryStrategy != null ? queryStrategy : new AsyncExecutionStrategy(); this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new AsyncSerialExecutionStrategy(); this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new AsyncExecutionStrategy(); this.instrumentation = instrumentation; this.valueUnboxer = valueUnboxer; this.doNotAutomaticallyDispatchDataLoader = doNotAutomaticallyDispatchDataLoader; + this.profiler = profiler; } public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState) { @@ -115,6 +119,7 @@ public CompletableFuture execute(Document document, GraphQLSche .executionInput(executionInput) .propagapropagateErrorsOnNonNullContractFailureeErrors(propagateErrorsOnNonNullContractFailure) .engineRunningObserver(engineRunningObserver) + .profiler(profiler) .build(); executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo()); diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 95abf8c6e3..d43af5a7dc 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,6 +8,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.Profiler; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.execution.EngineRunningObserver.RunningState; @@ -78,6 +79,7 @@ public class ExecutionContext { private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningObserver engineRunningObserver; + private final Profiler profiler; ExecutionContext(ExecutionContextBuilder builder) { this.graphQLSchema = builder.graphQLSchema; @@ -105,6 +107,7 @@ public class ExecutionContext { this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningObserver = builder.engineRunningObserver; + this.profiler = builder.profiler; } @@ -440,4 +443,8 @@ private void changeOfState(RunningState runningState) { engineRunningObserver.runningStateChanged(executionId, graphQLContext, runningState); } } + + public Profiler getProfiler() { + return profiler; + } } diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index fbd0cc7bf7..4c96970c77 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -7,6 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.Profiler; import graphql.collect.ImmutableKit; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; @@ -51,6 +52,7 @@ public class ExecutionContextBuilder { DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; boolean propagateErrorsOnNonNullContractFailure = true; EngineRunningObserver engineRunningObserver; + Profiler profiler; /** * @return a new builder of {@link graphql.execution.ExecutionContext}s @@ -99,6 +101,7 @@ public ExecutionContextBuilder() { dataLoaderDispatcherStrategy = other.getDataLoaderDispatcherStrategy(); propagateErrorsOnNonNullContractFailure = other.propagateErrorsOnNonNullContractFailure(); engineRunningObserver = other.getEngineRunningObserver(); + profiler = other.getProfiler(); } public ExecutionContextBuilder instrumentation(Instrumentation instrumentation) { @@ -245,4 +248,9 @@ public ExecutionContextBuilder engineRunningObserver(EngineRunningObserver engin this.engineRunningObserver = engineRunningObserver; return this; } + + public ExecutionContextBuilder profiler(Profiler profiler) { + this.profiler = profiler; + return this; + } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 44a086899e..3bba5d43f6 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -226,6 +226,7 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat if (fieldValueInfosResult instanceof CompletableFuture) { CompletableFuture> fieldValueInfos = (CompletableFuture>) fieldValueInfosResult; fieldValueInfos.whenComplete((completeValueInfos, throwable) -> { + executionContext.getProfiler().subSelectionCount(completeValueInfos.size()); executionContext.run(throwable, () -> { if (throwable != null) { handleResultsConsumer.accept(null, throwable); @@ -250,6 +251,7 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat return overallResult; } else { List completeValueInfos = (List) fieldValueInfosResult; + executionContext.getProfiler().subSelectionCount(completeValueInfos.size()); Async.CombinedBuilder resultFutures = fieldValuesCombinedBuilder(completeValueInfos); dataLoaderDispatcherStrategy.executeObjectOnFieldValuesInfo(completeValueInfos, parameters); @@ -478,6 +480,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); fetchCtx.onFetchedValue(fetchedObject); + executionContext.getProfiler().fieldFetched(fetchedObject, dataFetcher); // if it's a subscription, leave any reactive objects alone if (!executionContext.isSubscriptionOperation()) { // possible convert reactive objects into CompletableFutures diff --git a/src/test/groovy/graphql/execution/ExecutionTest.groovy b/src/test/groovy/graphql/execution/ExecutionTest.groovy index 7130beca0f..c56dcf0af1 100644 --- a/src/test/groovy/graphql/execution/ExecutionTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionTest.groovy @@ -35,7 +35,7 @@ class ExecutionTest extends Specification { def subscriptionStrategy = new CountingExecutionStrategy() def mutationStrategy = new CountingExecutionStrategy() def queryStrategy = new CountingExecutionStrategy() - def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimplePerformantInstrumentation.INSTANCE, ValueUnboxer.DEFAULT, false) + def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimplePerformantInstrumentation.INSTANCE, ValueUnboxer.DEFAULT, false, profiler) def emptyExecutionInput = ExecutionInput.newExecutionInput().query("query").build() def instrumentationState = new InstrumentationState() {} @@ -124,7 +124,7 @@ class ExecutionTest extends Specification { } } - def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, ValueUnboxer.DEFAULT, false) + def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, ValueUnboxer.DEFAULT, false, profiler) when: diff --git a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy index 376c5168fa..55d032000b 100644 --- a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy @@ -12,8 +12,6 @@ import graphql.execution.ExecutionId import graphql.execution.ResultPath import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation -import graphql.execution.instrumentation.SimplePerformantInstrumentation -import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -307,7 +305,7 @@ class FieldValidationTest extends Specification { def document = TestUtil.parseQuery(query) def strategy = new AsyncExecutionStrategy() def instrumentation = new FieldValidationInstrumentation(validation) - def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT, false) + def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT, false, profiler) def executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build() execution.execute(document, schema, ExecutionId.generate(), executionInput, null)