diff --git a/src/main/java/graphql/EngineRunningState.java b/src/main/java/graphql/EngineRunningState.java index 706a0e30b6..df1ee017ba 100644 --- a/src/main/java/graphql/EngineRunningState.java +++ b/src/main/java/graphql/EngineRunningState.java @@ -39,13 +39,18 @@ public class EngineRunningState { private final AtomicInteger isRunning = new AtomicInteger(0); - public EngineRunningState(ExecutionInput executionInput) { + + public EngineRunningState(ExecutionInput executionInput, Profiler profiler) { + EngineRunningObserver engineRunningObserver = executionInput.getGraphQLContext().get(EngineRunningObserver.ENGINE_RUNNING_OBSERVER_KEY); + EngineRunningObserver wrappedObserver = profiler.wrapEngineRunningObserver(engineRunningObserver); + this.engineRunningObserver = wrappedObserver; this.executionInput = executionInput; this.graphQLContext = executionInput.getGraphQLContext(); this.executionId = executionInput.getExecutionId(); - this.engineRunningObserver = executionInput.getGraphQLContext().get(EngineRunningObserver.ENGINE_RUNNING_OBSERVER_KEY); } + + public CompletableFuture handle(CompletableFuture src, BiFunction fn) { if (engineRunningObserver == null) { return src.handle(fn); diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 7bc9238701..f0db7e3057 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -34,6 +34,7 @@ public class ExecutionInput { private final ExecutionId executionId; private final Locale locale; private final AtomicBoolean cancelled; + private final boolean profileExecution; @Internal @@ -50,6 +51,7 @@ private ExecutionInput(Builder builder) { this.localContext = builder.localContext; this.extensions = builder.extensions; this.cancelled = builder.cancelled; + this.profileExecution = builder.profileExecution; } /** @@ -186,6 +188,11 @@ public void cancel() { cancelled.set(true); } + + 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. @@ -266,6 +273,7 @@ public static class Builder { private Locale locale = Locale.getDefault(); private ExecutionId executionId; private AtomicBoolean cancelled = new AtomicBoolean(false); + private boolean profileExecution; /** * Package level access to the graphql context @@ -414,6 +422,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 bee767ae43..16d14ab4b9 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -9,7 +9,6 @@ import graphql.execution.ExecutionId; import graphql.execution.ExecutionIdProvider; import graphql.execution.ExecutionStrategy; -import graphql.execution.ResponseMapFactory; import graphql.execution.SimpleDataFetcherExceptionHandler; import graphql.execution.SubscriptionExecutionStrategy; import graphql.execution.ValueUnboxer; @@ -27,10 +26,11 @@ import graphql.language.Document; import graphql.schema.GraphQLSchema; import graphql.validation.ValidationError; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; @@ -83,6 +83,7 @@ */ @SuppressWarnings("Duplicates") @PublicApi +@NullMarked public class GraphQL { /** @@ -259,9 +260,9 @@ public GraphQL transform(Consumer builderConsumer) { .queryExecutionStrategy(this.queryStrategy) .mutationExecutionStrategy(this.mutationStrategy) .subscriptionExecutionStrategy(this.subscriptionStrategy) - .executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider)) - .instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation)) - .preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider)); + .executionIdProvider(this.idProvider) + .instrumentation(this.instrumentation) + .preparsedDocumentProvider(this.preparsedDocumentProvider); builderConsumer.accept(builder); @@ -269,6 +270,7 @@ public GraphQL transform(Consumer builderConsumer) { } @PublicApi + @NullUnmarked public static class Builder { private GraphQLSchema graphQLSchema; private ExecutionStrategy queryExecutionStrategy; @@ -478,9 +480,11 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { - EngineRunningState engineRunningState = new EngineRunningState(executionInput); + Profiler profiler = executionInput.isProfileExecution() ? new ProfilerImpl(executionInput.getGraphQLContext()) : Profiler.NO_OP; + EngineRunningState engineRunningState = new EngineRunningState(executionInput, profiler); return engineRunningState.engineRun(() -> { ExecutionInput executionInputWithId = ensureInputHasId(executionInput); + profiler.setExecutionInputAndInstrumentation(executionInputWithId, instrumentation); engineRunningState.updateExecutionInput(executionInputWithId); CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInputWithId)); @@ -497,7 +501,7 @@ public CompletableFuture executeAsync(ExecutionInput executionI GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters, instrumentationState); - CompletableFuture executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState, engineRunningState); + CompletableFuture executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState, engineRunningState, profiler); // // finish up instrumentation executionResult = executionResult.whenComplete(completeInstrumentationCtxCF(executionInstrumentation)); @@ -529,7 +533,7 @@ private ExecutionInput ensureInputHasId(ExecutionInput executionInput) { } - private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, EngineRunningState engineRunningState) { + private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, EngineRunningState engineRunningState, 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 @@ -542,7 +546,7 @@ private CompletableFuture parseValidateAndExecute(ExecutionInpu return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDocumentEntry.getErrors())); } try { - return execute(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState, engineRunningState); + return execute(Assert.assertNotNull(executionInputRef.get()), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState, engineRunningState, profiler); } catch (AbortExecutionException e) { return CompletableFuture.completedFuture(e.toExecutionResult()); } @@ -551,7 +555,7 @@ private CompletableFuture parseValidateAndExecute(ExecutionInpu private PreparsedDocumentEntry parseAndValidate(AtomicReference executionInputRef, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - ExecutionInput executionInput = executionInputRef.get(); + ExecutionInput executionInput = assertNotNull(executionInputRef.get()); ParseAndValidateResult parseResult = parse(executionInput, graphQLSchema, instrumentationState); if (parseResult.isFailure()) { @@ -606,13 +610,14 @@ private CompletableFuture execute(ExecutionInput executionInput Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, - EngineRunningState engineRunningState + EngineRunningState engineRunningState, + Profiler profiler ) { Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader); ExecutionId executionId = executionInput.getExecutionId(); - return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState, engineRunningState); + return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState, engineRunningState, profiler); } } diff --git a/src/main/java/graphql/Profiler.java b/src/main/java/graphql/Profiler.java new file mode 100644 index 0000000000..6ac692a2fa --- /dev/null +++ b/src/main/java/graphql/Profiler.java @@ -0,0 +1,59 @@ +package graphql; + +import graphql.execution.EngineRunningObserver; +import graphql.execution.ResultPath; +import graphql.execution.instrumentation.Instrumentation; +import graphql.language.OperationDefinition; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLOutputType; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@Internal +@NullMarked +public interface Profiler { + + + Profiler NO_OP = new Profiler() { + }; + + + default void setExecutionInputAndInstrumentation(ExecutionInput executionInput, Instrumentation instrumentation) { + + } + + default void dataLoaderUsed(String dataLoaderName) { + + + } + + default void fieldFetched(Object fetchedObject, DataFetcher originalDataFetcher, DataFetcher dataFetcher, ResultPath path, GraphQLFieldDefinition fieldDef, GraphQLOutputType parentType) { + + } + + default @Nullable EngineRunningObserver wrapEngineRunningObserver(@Nullable EngineRunningObserver engineRunningObserver) { + return engineRunningObserver; + } + + default void operationDefinition(OperationDefinition operationDefinition) { + + } + + default void oldStrategyDispatchingAll(int level) { + + } + + default void batchLoadedOldStrategy(String name, int level, int count) { + + + } + + default void batchLoadedNewStrategy(String dataLoaderName, @Nullable Integer level, int count) { + + } + + default void manualDispatch(String dataLoaderName, int level, int count) { + + } +} diff --git a/src/main/java/graphql/ProfilerImpl.java b/src/main/java/graphql/ProfilerImpl.java new file mode 100644 index 0000000000..5c2e284dd5 --- /dev/null +++ b/src/main/java/graphql/ProfilerImpl.java @@ -0,0 +1,162 @@ +package graphql; + +import graphql.execution.EngineRunningObserver; +import graphql.execution.ExecutionId; +import graphql.execution.ResultPath; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatchingContextKeys; +import graphql.introspection.Introspection; +import graphql.language.OperationDefinition; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.PropertyDataFetcher; +import graphql.schema.SingletonPropertyDataFetcher; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; + +@Internal +@NullMarked +public class ProfilerImpl implements Profiler { + + private volatile long startTime; + private volatile long endTime; + private volatile long lastStartTime; + private final AtomicLong engineTotalRunningTime = new AtomicLong(); + + final ProfilerResult profilerResult = new ProfilerResult(); + + public ProfilerImpl(GraphQLContext graphQLContext) { + // No real work can happen here, since the engine didn't "officially" start yet. + graphQLContext.put(ProfilerResult.PROFILER_CONTEXT_KEY, profilerResult); + } + + @Override + public void setExecutionInputAndInstrumentation(ExecutionInput executionInput, Instrumentation instrumentation) { + profilerResult.setExecutionId(executionInput.getExecutionIdNonNull()); + boolean dataLoaderChainingEnabled = executionInput.getGraphQLContext().getBoolean(DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_CHAINING, false); + profilerResult.setDataLoaderChainingEnabled(dataLoaderChainingEnabled); + + List instrumentationClasses = new ArrayList<>(); + collectInstrumentationClasses(instrumentationClasses, instrumentation); + profilerResult.setInstrumentationClasses(instrumentationClasses); + } + + private void collectInstrumentationClasses(List result, Instrumentation instrumentation) { + if (instrumentation instanceof ChainedInstrumentation) { + ChainedInstrumentation chainedInstrumentation = (ChainedInstrumentation) instrumentation; + for (Instrumentation child : chainedInstrumentation.getInstrumentations()) { + collectInstrumentationClasses(result, child); + } + } else { + result.add(instrumentation.getClass().getName()); + } + } + + + @Override + public void fieldFetched(Object fetchedObject, DataFetcher originalDataFetcher, DataFetcher dataFetcher, ResultPath path, GraphQLFieldDefinition fieldDef, GraphQLOutputType parentType) { + String key = "/" + String.join("/", path.getKeysOnly()); + if (Introspection.isIntrospectionTypes(GraphQLTypeUtil.unwrapAll(fieldDef.getType())) + || Introspection.isIntrospectionTypes(GraphQLTypeUtil.unwrapAll(parentType)) + || fieldDef.getName().equals(Introspection.SchemaMetaFieldDef.getName()) + || fieldDef.getName().equals(Introspection.TypeMetaFieldDef.getName()) + || fieldDef.getName().equals(Introspection.TypeNameMetaFieldDef.getName())) { + return; + } + profilerResult.addFieldFetched(key); + profilerResult.incrementDataFetcherInvocationCount(key); + ProfilerResult.DataFetcherType dataFetcherType; + if (dataFetcher instanceof PropertyDataFetcher || dataFetcher instanceof SingletonPropertyDataFetcher) { + dataFetcherType = ProfilerResult.DataFetcherType.TRIVIAL_DATA_FETCHER; + } else if (originalDataFetcher instanceof PropertyDataFetcher || originalDataFetcher instanceof SingletonPropertyDataFetcher) { + dataFetcherType = ProfilerResult.DataFetcherType.WRAPPED_TRIVIAL_DATA_FETCHER; + } else { + dataFetcherType = ProfilerResult.DataFetcherType.CUSTOM; + // we only record the type of the result if it is not a PropertyDataFetcher + ProfilerResult.DataFetcherResultType dataFetcherResultType; + if (fetchedObject instanceof CompletableFuture) { + CompletableFuture completableFuture = (CompletableFuture) fetchedObject; + if (completableFuture.isDone()) { + dataFetcherResultType = ProfilerResult.DataFetcherResultType.COMPLETABLE_FUTURE_COMPLETED; + } else { + dataFetcherResultType = ProfilerResult.DataFetcherResultType.COMPLETABLE_FUTURE_NOT_COMPLETED; + } + } else { + dataFetcherResultType = ProfilerResult.DataFetcherResultType.MATERIALIZED; + } + profilerResult.setDataFetcherResultType(key, dataFetcherResultType); + } + + profilerResult.setDataFetcherType(key, dataFetcherType); + } + + @Override + public EngineRunningObserver wrapEngineRunningObserver(@Nullable EngineRunningObserver engineRunningObserver) { + // nothing to wrap here + return new EngineRunningObserver() { + @Override + public void runningStateChanged(@Nullable ExecutionId executionId, GraphQLContext graphQLContext, RunningState runningState) { + runningStateChangedImpl(executionId, graphQLContext, runningState); + if (engineRunningObserver != null) { + engineRunningObserver.runningStateChanged(executionId, graphQLContext, runningState); + } + } + }; + } + + private void runningStateChangedImpl(@Nullable ExecutionId executionId, GraphQLContext graphQLContext, EngineRunningObserver.RunningState runningState) { + long now = System.nanoTime(); + if (runningState == EngineRunningObserver.RunningState.RUNNING_START) { + startTime = now; + lastStartTime = startTime; + } else if (runningState == EngineRunningObserver.RunningState.NOT_RUNNING_FINISH) { + endTime = now; + engineTotalRunningTime.set(engineTotalRunningTime.get() + (endTime - lastStartTime)); + profilerResult.setTimes(startTime, endTime, engineTotalRunningTime.get()); + } else if (runningState == EngineRunningObserver.RunningState.RUNNING) { + lastStartTime = now; + } else if (runningState == EngineRunningObserver.RunningState.NOT_RUNNING) { + engineTotalRunningTime.set(engineTotalRunningTime.get() + (now - lastStartTime)); + } else { + Assert.assertShouldNeverHappen(); + } + } + + @Override + public void operationDefinition(OperationDefinition operationDefinition) { + profilerResult.setOperation(operationDefinition); + } + + @Override + public void dataLoaderUsed(String dataLoaderName) { + profilerResult.addDataLoaderUsed(dataLoaderName); + } + + @Override + public void oldStrategyDispatchingAll(int level) { + profilerResult.oldStrategyDispatchingAll(level); + } + + @Override + public void batchLoadedOldStrategy(String name, int level, int count) { + profilerResult.addDispatchEvent(name, level, count, ProfilerResult.DispatchEventType.STRATEGY_DISPATCH); + } + + @Override + public void batchLoadedNewStrategy(String dataLoaderName, @Nullable Integer level, int count) { + profilerResult.addDispatchEvent(dataLoaderName, level, count, ProfilerResult.DispatchEventType.STRATEGY_DISPATCH); + } + + @Override + public void manualDispatch(String dataLoaderName, int level, int count) { + profilerResult.addDispatchEvent(dataLoaderName, level, count, ProfilerResult.DispatchEventType.MANUAL_DISPATCH); + } +} diff --git a/src/main/java/graphql/ProfilerResult.java b/src/main/java/graphql/ProfilerResult.java new file mode 100644 index 0000000000..a10e55c8a1 --- /dev/null +++ b/src/main/java/graphql/ProfilerResult.java @@ -0,0 +1,358 @@ +package graphql; + +import graphql.execution.ExecutionId; +import graphql.language.OperationDefinition; +import graphql.language.OperationDefinition.Operation; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@ExperimentalApi +@NullMarked +public class ProfilerResult { + + public static final String PROFILER_CONTEXT_KEY = "__GJ_PROFILER"; + + @Nullable + private volatile ExecutionId executionId; + private long startTime; + private long endTime; + private long engineTotalRunningTime; + private final AtomicInteger totalDataFetcherInvocations = new AtomicInteger(); + private final AtomicInteger totalTrivialDataFetcherInvocations = new AtomicInteger(); + private final AtomicInteger totalWrappedTrivialDataFetcherInvocations = new AtomicInteger(); + + // this is the count of how many times a data loader was invoked per data loader name + private final Map dataLoaderLoadInvocations = new ConcurrentHashMap<>(); + + @Nullable + private volatile String operationName; + @Nullable + private volatile Operation operationType; + private volatile boolean dataLoaderChainingEnabled; + private final Set oldStrategyDispatchingAll = ConcurrentHashMap.newKeySet(); + + private final List instrumentationClasses = Collections.synchronizedList(new ArrayList<>()); + + private final List dispatchEvents = Collections.synchronizedList(new ArrayList<>()); + + /** + * the following fields can contain a lot of data for large requests + */ + // all fields fetched during the execution, key is the field path + private final Set fieldsFetched = ConcurrentHashMap.newKeySet(); + // this is the count of how many times a data fetcher was invoked per field + private final Map dataFetcherInvocationCount = new ConcurrentHashMap<>(); + // the type of the data fetcher per field, key is the field path + private final Map dataFetcherTypeMap = new ConcurrentHashMap<>(); + // the type of the data fetcher result field, key is the field path + // in theory different DataFetcher invocations can return different types, but we only record the first one + private final Map dataFetcherResultType = new ConcurrentHashMap<>(); + + public void setInstrumentationClasses(List instrumentationClasses) { + this.instrumentationClasses.addAll(instrumentationClasses); + } + + + public enum DispatchEventType { + STRATEGY_DISPATCH, + MANUAL_DISPATCH, + } + + public static class DispatchEvent { + final String dataLoaderName; + final @Nullable Integer level; // is null for delayed dispatching + final int keyCount; // how many + final DispatchEventType type; + + public DispatchEvent(String dataLoaderName, @Nullable Integer level, int keyCount, DispatchEventType type) { + this.dataLoaderName = dataLoaderName; + this.level = level; + this.keyCount = keyCount; + this.type = type; + } + + public String getDataLoaderName() { + return dataLoaderName; + } + + public @Nullable Integer getLevel() { + return level; + } + + public int getKeyCount() { + return keyCount; + } + + public DispatchEventType getType() { + return type; + } + + @Override + public String toString() { + return "DispatchEvent{" + + "type=" + type + + ", dataLoaderName='" + dataLoaderName + '\'' + + ", level=" + level + + ", keyCount=" + keyCount + + '}'; + } + } + + public enum DataFetcherType { + WRAPPED_TRIVIAL_DATA_FETCHER, + TRIVIAL_DATA_FETCHER, + CUSTOM + } + + public enum DataFetcherResultType { + COMPLETABLE_FUTURE_COMPLETED, + COMPLETABLE_FUTURE_NOT_COMPLETED, + MATERIALIZED + + } + + + // setters are package private to prevent exposure + + void setDataLoaderChainingEnabled(boolean dataLoaderChainingEnabled) { + this.dataLoaderChainingEnabled = dataLoaderChainingEnabled; + } + + + void setDataFetcherType(String key, DataFetcherType dataFetcherType) { + dataFetcherTypeMap.putIfAbsent(key, dataFetcherType); + totalDataFetcherInvocations.incrementAndGet(); + if (dataFetcherType == DataFetcherType.TRIVIAL_DATA_FETCHER) { + totalTrivialDataFetcherInvocations.incrementAndGet(); + } else if (dataFetcherType == DataFetcherType.WRAPPED_TRIVIAL_DATA_FETCHER) { + totalWrappedTrivialDataFetcherInvocations.incrementAndGet(); + } + } + + void setDataFetcherResultType(String key, DataFetcherResultType fetchedType) { + dataFetcherResultType.putIfAbsent(key, fetchedType); + } + + void incrementDataFetcherInvocationCount(String key) { + dataFetcherInvocationCount.compute(key, (k, v) -> v == null ? 1 : v + 1); + } + + void addFieldFetched(String fieldPath) { + fieldsFetched.add(fieldPath); + } + + void setExecutionId(ExecutionId executionId) { + this.executionId = executionId; + } + + void setTimes(long startTime, long endTime, long engineTotalRunningTime) { + this.startTime = startTime; + this.endTime = endTime; + this.engineTotalRunningTime = engineTotalRunningTime; + } + + void setOperation(OperationDefinition operationDefinition) { + this.operationName = operationDefinition.getName(); + this.operationType = operationDefinition.getOperation(); + } + + void addDataLoaderUsed(String dataLoaderName) { + dataLoaderLoadInvocations.compute(dataLoaderName, (k, v) -> v == null ? 1 : v + 1); + } + + void oldStrategyDispatchingAll(int level) { + oldStrategyDispatchingAll.add(level); + } + + + void addDispatchEvent(String dataLoaderName, @Nullable Integer level, int count, DispatchEventType type) { + dispatchEvents.add(new DispatchEvent(dataLoaderName, level, count, type)); + } + + // public getters + + public @Nullable String getOperationName() { + return operationName; + } + + public Operation getOperationType() { + return Assert.assertNotNull(operationType); + } + + public Set getFieldsFetched() { + return fieldsFetched; + } + + public Set getCustomDataFetcherFields() { + Set result = new LinkedHashSet<>(fieldsFetched); + for (String field : fieldsFetched) { + if (dataFetcherTypeMap.get(field) == DataFetcherType.CUSTOM) { + result.add(field); + } + } + return result; + } + + public Set getTrivialDataFetcherFields() { + Set result = new LinkedHashSet<>(fieldsFetched); + for (String field : fieldsFetched) { + if (dataFetcherTypeMap.get(field) == DataFetcherType.TRIVIAL_DATA_FETCHER) { + result.add(field); + } + } + return result; + } + + + public int getTotalDataFetcherInvocations() { + return totalDataFetcherInvocations.get(); + } + + public int getTotalTrivialDataFetcherInvocations() { + return totalTrivialDataFetcherInvocations.get(); + } + + public int getTotalCustomDataFetcherInvocations() { + return totalDataFetcherInvocations.get() - totalTrivialDataFetcherInvocations.get() - totalWrappedTrivialDataFetcherInvocations.get(); + } + + public long getStartTime() { + return startTime; + } + + public long getEndTime() { + return endTime; + } + + public long getEngineTotalRunningTime() { + return engineTotalRunningTime; + } + + public long getTotalExecutionTime() { + return endTime - startTime; + } + + public Map getDataFetcherResultType() { + return dataFetcherResultType; + } + + public Map getDataLoaderLoadInvocations() { + return dataLoaderLoadInvocations; + } + + + public Set getOldStrategyDispatchingAll() { + return oldStrategyDispatchingAll; + } + + public boolean isDataLoaderChainingEnabled() { + return dataLoaderChainingEnabled; + } + + public List getDispatchEvents() { + return dispatchEvents; + } + + public List getInstrumentationClasses() { + return instrumentationClasses; + } + + + public Map shortSummaryMap() { + Map result = new LinkedHashMap<>(); + result.put("executionId", Assert.assertNotNull(executionId)); + result.put("operation", operationType + ":" + operationName); + result.put("startTime", startTime); + result.put("endTime", endTime); + result.put("totalRunTime", (endTime - startTime) + "(" + (endTime - startTime) / 1_000_000 + "ms)"); + result.put("engineTotalRunningTime", engineTotalRunningTime + "(" + engineTotalRunningTime / 1_000_000 + "ms)"); + result.put("totalDataFetcherInvocations", totalDataFetcherInvocations); + result.put("totalCustomDataFetcherInvocations", getTotalCustomDataFetcherInvocations()); + result.put("totalTrivialDataFetcherInvocations", totalTrivialDataFetcherInvocations); + result.put("totalWrappedTrivialDataFetcherInvocations", totalWrappedTrivialDataFetcherInvocations); + result.put("fieldsFetchedCount", fieldsFetched.size()); + result.put("dataLoaderChainingEnabled", dataLoaderChainingEnabled); + result.put("dataLoaderLoadInvocations", dataLoaderLoadInvocations); + result.put("oldStrategyDispatchingAll", oldStrategyDispatchingAll); + result.put("dispatchEvents", getDispatchEventsAsMap()); + result.put("instrumentationClasses", instrumentationClasses); + int completedCount = 0; + int completedInvokeCount = 0; + int notCompletedCount = 0; + int notCompletedInvokeCount = 0; + int materializedCount = 0; + int materializedInvokeCount = 0; + for (String field : dataFetcherResultType.keySet()) { + DataFetcherResultType dFRT = dataFetcherResultType.get(field); + if (dFRT == DataFetcherResultType.COMPLETABLE_FUTURE_COMPLETED) { + completedInvokeCount += Assert.assertNotNull(dataFetcherInvocationCount.get(field)); + completedCount++; + } else if (dFRT == DataFetcherResultType.COMPLETABLE_FUTURE_NOT_COMPLETED) { + notCompletedInvokeCount += Assert.assertNotNull(dataFetcherInvocationCount.get(field)); + notCompletedCount++; + } else if (dFRT == DataFetcherResultType.MATERIALIZED) { + materializedInvokeCount += Assert.assertNotNull(dataFetcherInvocationCount.get(field)); + materializedCount++; + } else { + Assert.assertShouldNeverHappen(); + } + } + result.put("dataFetcherResultTypes", Map.of( + DataFetcherResultType.COMPLETABLE_FUTURE_COMPLETED.name(), "(count:" + completedCount + ", invocations:" + completedInvokeCount + ")", + DataFetcherResultType.COMPLETABLE_FUTURE_NOT_COMPLETED.name(), "(count:" + notCompletedCount + ", invocations:" + notCompletedInvokeCount + ")", + DataFetcherResultType.MATERIALIZED.name(), "(count:" + materializedCount + ", invocations:" + materializedInvokeCount + ")" + )); + return result; + } + + + private String printDispatchEvents() { + if (dispatchEvents.isEmpty()) { + return "[]"; + } + StringBuilder sb = new StringBuilder(); + sb.append("["); + int i = 0; + for (DispatchEvent event : dispatchEvents) { + sb.append("dataLoader=") + .append(event.getDataLoaderName()) + .append(", level=") + .append(event.getLevel()) + .append(", count=").append(event.getKeyCount()); + if (i++ < dispatchEvents.size() - 1) { + sb.append("; "); + } + } + sb.append("]"); + return sb.toString(); + } + + public List> getDispatchEventsAsMap() { + List> result = new ArrayList<>(); + for (DispatchEvent event : dispatchEvents) { + Map eventMap = new LinkedHashMap<>(); + eventMap.put("type", event.getType().name()); + eventMap.put("dataLoader", event.getDataLoaderName()); + eventMap.put("level", event.getLevel() != null ? event.getLevel() : "delayed"); + eventMap.put("keyCount", event.getKeyCount()); + result.add(eventMap); + } + return result; + } + + @Override + public String toString() { + return "ProfilerResult" + shortSummaryMap(); + } + +} diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java index 13d918bdd9..b3f837cd5c 100644 --- a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -57,9 +57,6 @@ default void fieldFetched(ExecutionContext executionContext, } - default DataFetcher modifyDataFetcher(DataFetcher dataFetcher) { - return dataFetcher; - } default void newSubscriptionExecution(FieldValueInfo fieldValueInfo, AlternativeCallContext alternativeCallContext) { diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 4c8cb7da3d..9245a94576 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -10,6 +10,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; @@ -71,7 +72,7 @@ public Execution(ExecutionStrategy queryStrategy, this.doNotAutomaticallyDispatchDataLoader = doNotAutomaticallyDispatchDataLoader; } - public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState, EngineRunningState engineRunningState) { + public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState, EngineRunningState engineRunningState, Profiler profiler) { NodeUtil.GetOperationResult getOperationResult; CoercedVariables coercedVariables; Supplier normalizedVariableValues; @@ -121,6 +122,7 @@ public CompletableFuture execute(Document document, GraphQLSche .executionInput(executionInput) .propagapropagateErrorsOnNonNullContractFailureeErrors(propagateErrorsOnNonNullContractFailure) .engineRunningState(engineRunningState) + .profiler(profiler) .build(); executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo()); @@ -162,7 +164,7 @@ private CompletableFuture executeOperation(ExecutionContext exe OperationDefinition.Operation operation = operationDefinition.getOperation(); GraphQLObjectType operationRootType; - + executionContext.getProfiler().operationDefinition(operationDefinition); try { operationRootType = SchemaUtil.getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); } catch (RuntimeException rte) { diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 5bdc076d36..c6a7edc916 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -9,6 +9,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.Profiler; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.execution.incremental.IncrementalCallState; @@ -30,7 +31,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; @@ -68,14 +68,14 @@ public class ExecutionContext { private final Supplier queryTree; private final boolean propagateErrorsOnNonNullContractFailure; - private final AtomicInteger isRunning = new AtomicInteger(0); - // this is modified after creation so it needs to be volatile to ensure visibility across Threads private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningState engineRunningState; + private final Profiler profiler; + ExecutionContext(ExecutionContextBuilder builder) { this.graphQLSchema = builder.graphQLSchema; this.executionId = builder.executionId; @@ -103,6 +103,7 @@ public class ExecutionContext { this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; + this.profiler = builder.profiler; } public ExecutionId getExecutionId() { @@ -385,8 +386,14 @@ Throwable possibleCancellation(@Nullable Throwable currentThrowable) { return engineRunningState.possibleCancellation(currentThrowable); } + + @Internal + public Profiler getProfiler() { + return profiler; + } + @Internal void throwIfCancelled() throws AbortExecutionException { engineRunningState.throwIfCancelled(); } -} \ No newline at end of file +} diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index fc28675f09..86ec2bfd60 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -8,6 +8,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; @@ -53,6 +54,7 @@ public class ExecutionContextBuilder { boolean propagateErrorsOnNonNullContractFailure = true; EngineRunningState engineRunningState; ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT; + Profiler profiler; /** * @return a new builder of {@link graphql.execution.ExecutionContext}s @@ -102,6 +104,7 @@ public ExecutionContextBuilder() { propagateErrorsOnNonNullContractFailure = other.propagateErrorsOnNonNullContractFailure(); engineRunningState = other.getEngineRunningState(); responseMapFactory = other.getResponseMapFactory(); + profiler = other.getProfiler(); } public ExecutionContextBuilder instrumentation(Instrumentation instrumentation) { @@ -254,4 +257,9 @@ public ExecutionContextBuilder engineRunningState(EngineRunningState engineRunni this.engineRunningState = engineRunningState; 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 614b75e703..a16f0f80f1 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -447,18 +447,17 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec }); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); + DataFetcher originalDataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); + InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, originalDataFetcher instanceof TrivialDataFetcher); FieldFetchingInstrumentationContext fetchCtx = FieldFetchingInstrumentationContext.nonNullCtx(instrumentation.beginFieldFetching(instrumentationFieldFetchParams, executionContext.getInstrumentationState()) ); - dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); - dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); - Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); + DataFetcher dataFetcher = instrumentation.instrumentDataFetcher(originalDataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); + Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, originalDataFetcher, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject, dataFetchingEnvironment); fetchCtx.onDispatched(); fetchCtx.onFetchedValue(fetchedObject); @@ -497,7 +496,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec * ExecutionContext is not used in the method, but the java agent uses it, so it needs to be present */ @SuppressWarnings("unused") - private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { + private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher originalDataFetcher, DataFetcher dataFetcher) { Object fetchedValue; try { Object fetchedValueRaw; @@ -506,6 +505,7 @@ private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStr } else { fetchedValueRaw = dataFetcher.get(dataFetchingEnvironment.get()); } + executionContext.getProfiler().fieldFetched(fetchedValueRaw, originalDataFetcher, dataFetcher, parameters.getPath(), fieldDef, parameters.getExecutionStepInfo().getType()); fetchedValue = Async.toCompletableFutureOrMaterializedObject(fetchedValueRaw); } catch (Exception e) { fetchedValue = Async.exceptionallyCompletedFuture(e); diff --git a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java index 4058f2f38b..422d0ece71 100644 --- a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java @@ -1,6 +1,8 @@ package graphql.execution.instrumentation; import graphql.PublicSpi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * When a {@link Instrumentation}.'beginXXX()' method is called then it must return a non null InstrumentationContext @@ -11,6 +13,7 @@ * just happened or "loggers" to be called to record what has happened. */ @PublicSpi +@NullMarked public interface InstrumentationContext { /** @@ -24,6 +27,6 @@ public interface InstrumentationContext { * @param result the result of the step (which may be null) * @param t this exception will be non null if an exception was thrown during the step */ - void onCompleted(T result, Throwable t); + void onCompleted(@Nullable T result, @Nullable Throwable t); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 68da3a3528..efc5a98073 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -3,6 +3,7 @@ import graphql.Assert; import graphql.GraphQLContext; import graphql.Internal; +import graphql.Profiler; import graphql.execution.DataLoaderDispatchStrategy; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -47,6 +48,7 @@ public class PerLevelDataLoaderDispatchStrategy implements DataLoaderDispatchStr = new InterThreadMemoizedSupplier<>(() -> Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors())); static final long DEFAULT_BATCH_WINDOW_NANO_SECONDS_DEFAULT = 500_000L; + private final Profiler profiler; private final Map deferredCallStackMap = new ConcurrentHashMap<>(); @@ -218,6 +220,7 @@ public PerLevelDataLoaderDispatchStrategy(ExecutionContext executionContext) { }); this.enableDataLoaderChaining = graphQLContext.getBoolean(DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_CHAINING, false); + this.profiler = executionContext.getProfiler(); } @@ -485,11 +488,11 @@ private boolean checkLevelImpl(int level, CallStack callStack) { void dispatch(int level, CallStack callStack) { if (!enableDataLoaderChaining) { + profiler.oldStrategyDispatchingAll(level); DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); - dataLoaderRegistry.dispatchAll(); + dispatchAll(dataLoaderRegistry, level); return; } - Set resultPathWithDataLoaders = callStack.levelToResultPathWithDataLoader.get(level); if (resultPathWithDataLoaders != null) { Set resultPathToDispatch = callStack.lock.callLocked(() -> { @@ -508,6 +511,16 @@ void dispatch(int level, CallStack callStack) { } } + private void dispatchAll(DataLoaderRegistry dataLoaderRegistry, int level) { + for (DataLoader dataLoader : dataLoaderRegistry.getDataLoaders()) { + dataLoader.dispatch().whenComplete((objects, throwable) -> { + if (objects != null && objects.size() > 0) { + Assert.assertNotNull(dataLoader.getName()); + profiler.batchLoadedOldStrategy(dataLoader.getName(), level, objects.size()); + } + }); + } + } private void dispatchDLCFImpl(Set resultPathsToDispatch, @Nullable Integer level, CallStack callStack) { @@ -533,7 +546,13 @@ private void dispatchDLCFImpl(Set resultPathsToDispatch, @Nullable Integ } List allDispatchedCFs = new ArrayList<>(); for (ResultPathWithDataLoader resultPathWithDataLoader : relevantResultPathWithDataLoader) { - allDispatchedCFs.add(resultPathWithDataLoader.dataLoader.dispatch()); + CompletableFuture dispatch = resultPathWithDataLoader.dataLoader.dispatch(); + allDispatchedCFs.add(dispatch); + dispatch.whenComplete((objects, throwable) -> { + if (objects != null && objects.size() > 0) { + profiler.batchLoadedNewStrategy(resultPathWithDataLoader.name, level, objects.size()); + } + }); } CompletableFuture.allOf(allDispatchedCFs.toArray(new CompletableFuture[0])) .whenComplete((unused, throwable) -> { diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java index b0907d3aa6..5cf2685d1f 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java @@ -5,6 +5,7 @@ import graphql.Assert; import graphql.GraphQLContext; import graphql.Internal; +import graphql.Profiler; import graphql.collect.ImmutableKit; import graphql.collect.ImmutableMapWithNullValues; import graphql.execution.DataLoaderDispatchStrategy; @@ -87,7 +88,7 @@ private DataFetchingEnvironmentImpl(Builder builder) { this.queryDirectives = builder.queryDirectives; // internal state - this.dfeInternalState = new DFEInternalState(builder.dataLoaderDispatchStrategy, builder.alternativeCallContext); + this.dfeInternalState = new DFEInternalState(builder.dataLoaderDispatchStrategy, builder.alternativeCallContext, builder.profiler); } /** @@ -114,7 +115,9 @@ public static Builder newDataFetchingEnvironment(ExecutionContext executionConte .operationDefinition(executionContext.getOperationDefinition()) .variables(executionContext.getCoercedVariables().toMap()) .executionId(executionContext.getExecutionId()) - .dataLoaderDispatchStrategy(executionContext.getDataLoaderDispatcherStrategy()); + .dataLoaderDispatchStrategy(executionContext.getDataLoaderDispatcherStrategy()) + .profiler(executionContext.getProfiler()); + } @Override @@ -299,6 +302,7 @@ public static class Builder { private ImmutableMapWithNullValues variables; private QueryDirectives queryDirectives; private DataLoaderDispatchStrategy dataLoaderDispatchStrategy; + private Profiler profiler; private AlternativeCallContext alternativeCallContext; public Builder(DataFetchingEnvironmentImpl env) { @@ -324,6 +328,7 @@ public Builder(DataFetchingEnvironmentImpl env) { this.variables = env.variables; this.queryDirectives = env.queryDirectives; this.dataLoaderDispatchStrategy = env.dfeInternalState.dataLoaderDispatchStrategy; + this.profiler = env.dfeInternalState.profiler; this.alternativeCallContext = env.dfeInternalState.alternativeCallContext; } @@ -457,16 +462,23 @@ public Builder dataLoaderDispatchStrategy(DataLoaderDispatchStrategy dataLoaderD this.dataLoaderDispatchStrategy = dataLoaderDispatcherStrategy; return this; } + + public Builder profiler(Profiler profiler) { + this.profiler = profiler; + return this; + } } @Internal public static class DFEInternalState { final DataLoaderDispatchStrategy dataLoaderDispatchStrategy; + final Profiler profiler; final AlternativeCallContext alternativeCallContext; - public DFEInternalState(DataLoaderDispatchStrategy dataLoaderDispatchStrategy, AlternativeCallContext alternativeCallContext) { + public DFEInternalState(DataLoaderDispatchStrategy dataLoaderDispatchStrategy, AlternativeCallContext alternativeCallContext, Profiler profiler) { this.dataLoaderDispatchStrategy = dataLoaderDispatchStrategy; this.alternativeCallContext = alternativeCallContext; + this.profiler = profiler; } public DataLoaderDispatchStrategy getDataLoaderDispatchStrategy() { @@ -476,5 +488,9 @@ public DataLoaderDispatchStrategy getDataLoaderDispatchStrategy() { public AlternativeCallContext getDeferredCallContext() { return alternativeCallContext; } + + public Profiler getProfiler() { + return profiler; + } } } diff --git a/src/main/java/graphql/schema/DataLoaderWithContext.java b/src/main/java/graphql/schema/DataLoaderWithContext.java index a9086e334a..b985a8eafc 100644 --- a/src/main/java/graphql/schema/DataLoaderWithContext.java +++ b/src/main/java/graphql/schema/DataLoaderWithContext.java @@ -9,6 +9,7 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.List; import java.util.concurrent.CompletableFuture; @Internal @@ -30,6 +31,7 @@ public CompletableFuture load(@NonNull K key, @Nullable Object keyContext) { CompletableFuture result = super.load(key, keyContext); DataFetchingEnvironmentImpl dfeImpl = (DataFetchingEnvironmentImpl) dfe; DataFetchingEnvironmentImpl.DFEInternalState dfeInternalState = (DataFetchingEnvironmentImpl.DFEInternalState) dfeImpl.toInternal(); + dfeInternalState.getProfiler().dataLoaderUsed(dataLoaderName); if (dfeInternalState.getDataLoaderDispatchStrategy() instanceof PerLevelDataLoaderDispatchStrategy) { AlternativeCallContext alternativeCallContext = dfeInternalState.getDeferredCallContext(); int level = dfe.getExecutionStepInfo().getPath().getLevel(); @@ -39,4 +41,15 @@ public CompletableFuture load(@NonNull K key, @Nullable Object keyContext) { return result; } + @Override + public CompletableFuture> dispatch() { + CompletableFuture> dispatchResult = delegate.dispatch(); + dispatchResult.whenComplete((result, error) -> { + if (result != null && result.size() > 0) { + DataFetchingEnvironmentImpl.DFEInternalState dfeInternalState = (DataFetchingEnvironmentImpl.DFEInternalState) dfe.toInternal(); + dfeInternalState.getProfiler().manualDispatch(dataLoaderName, dfe.getExecutionStepInfo().getPath().getLevel(), result.size()); + } + }); + return dispatchResult; + } } diff --git a/src/test/groovy/graphql/ProfilerTest.groovy b/src/test/groovy/graphql/ProfilerTest.groovy new file mode 100644 index 0000000000..25b58a6068 --- /dev/null +++ b/src/test/groovy/graphql/ProfilerTest.groovy @@ -0,0 +1,590 @@ +package graphql + +import graphql.execution.instrumentation.ChainedInstrumentation +import graphql.execution.instrumentation.Instrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.OperationDefinition +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.awaitility.Awaitility +import org.dataloader.BatchLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.dataloader.DataLoaderRegistry +import spock.lang.Specification + +import java.time.Duration +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger + +import static graphql.ExecutionInput.newExecutionInput +import static graphql.ProfilerResult.DataFetcherResultType.COMPLETABLE_FUTURE_COMPLETED +import static graphql.ProfilerResult.DataFetcherResultType.COMPLETABLE_FUTURE_NOT_COMPLETED +import static graphql.ProfilerResult.DataFetcherResultType.MATERIALIZED +import static graphql.execution.instrumentation.dataloader.DataLoaderDispatchingContextKeys.setEnableDataLoaderChaining +import static java.util.concurrent.CompletableFuture.supplyAsync + +class ProfilerTest extends Specification { + + + def "one field"() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl, [Query: [ + hello: { DataFetchingEnvironment dfe -> return "world" } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ hello }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [hello: "world"] + + then: + profilerResult.getFieldsFetched() == ["/hello"] as Set + + } + + def "introspection fields are ignored"() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl, [Query: [ + hello: { DataFetchingEnvironment dfe -> return "world" } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ hello __typename alias:__typename __schema {types{name}} __type(name: \"Query\") {name} }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData()["hello"] == "world" + + then: + profilerResult.getFieldsFetched() == ["/hello",] as Set + profilerResult.getTotalDataFetcherInvocations() == 1 + + } + + def "pure introspection "() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl, [Query: [ + hello: { DataFetchingEnvironment dfe -> return "world" } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ __schema {types{name}} __type(name: \"Query\") {name} }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData()["__schema"] != null + + then: + profilerResult.getFieldsFetched() == [] as Set + profilerResult.getTotalDataFetcherInvocations() == 0 + + } + + + def "instrumented data fetcher"() { + given: + def sdl = ''' + type Query { + dog: Dog + } + type Dog { + name: String + age: Int + } + ''' + + + def dogDf = { DataFetchingEnvironment dfe -> return [name: "Luna", age: 5] } as DataFetcher + + Instrumentation instrumentation = new Instrumentation() { + @Override + DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + if (parameters.getField().getName() == "name") { + // wrapping a PropertyDataFetcher + return { DataFetchingEnvironment dfe -> + def result = dataFetcher.get(dfe) + return result + } as DataFetcher + } + return dataFetcher + } + + } + def dfs = [Query: [ + dog: dogDf + ]] + def schema = TestUtil.schema(sdl, dfs) + def graphql = GraphQL.newGraphQL(schema).instrumentation(instrumentation).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ dog {name age} }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [dog: [name: "Luna", age: 5]] + + then: + profilerResult.getTotalDataFetcherInvocations() == 3 + profilerResult.getTotalTrivialDataFetcherInvocations() == 1 + profilerResult.getTotalTrivialDataFetcherInvocations() == 1 + profilerResult.getTotalCustomDataFetcherInvocations() == 1 + profilerResult.getDataFetcherResultType() == ["/dog": MATERIALIZED] + } + + + def "manual dataloader dispatch"() { + given: + def sdl = ''' + + type Query { + dog: String + } + ''' + AtomicInteger batchLoadCalls = new AtomicInteger() + BatchLoader batchLoader = { keys -> + return supplyAsync { + batchLoadCalls.incrementAndGet() + Thread.sleep(250) + println "BatchLoader called with keys: $keys" + return ["Luna"] + } + } + + DataLoader nameDataLoader = DataLoaderFactory.newDataLoader(batchLoader); + + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + dataLoaderRegistry.register("name", nameDataLoader); + + def df1 = { env -> + def loader = env.getDataLoader("name") + def result = loader.load("Key1") + loader.dispatch() + return result + } as DataFetcher + + def fetchers = ["Query": ["dog": df1]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ dog } " + def ei = newExecutionInput(query).dataLoaderRegistry(dataLoaderRegistry).profileExecution(true).build() + setEnableDataLoaderChaining(ei.graphQLContext, true) + + when: + def efCF = graphQL.executeAsync(ei) + Awaitility.await().until { efCF.isDone() } + def er = efCF.get() + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + then: + er.data == [dog: "Luna"] + batchLoadCalls.get() == 1 + then: + profilerResult.getDispatchEvents()[0].type == ProfilerResult.DispatchEventType.MANUAL_DISPATCH + profilerResult.getDispatchEvents()[0].dataLoaderName == "name" + profilerResult.getDispatchEvents()[0].keyCount == 1 + profilerResult.getDispatchEvents()[0].level == 1 + + } + + def "cached dataloader values"() { + given: + def sdl = ''' + + type Query { + dog: Dog + } + type Dog { + name: String + } + ''' + AtomicInteger batchLoadCalls = new AtomicInteger() + BatchLoader batchLoader = { keys -> + return supplyAsync { + batchLoadCalls.incrementAndGet() + Thread.sleep(250) + println "BatchLoader called with keys: $keys" + return ["Luna"] + } + } + + DataLoader nameDataLoader = DataLoaderFactory.newDataLoader(batchLoader); + + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + dataLoaderRegistry.register("name", nameDataLoader); + + def dogDF = { env -> + def loader = env.getDataLoader("name") + def result = loader.load("Key1").thenCompose { + return loader.load("Key1") // This should hit the cache + } + } as DataFetcher + + def nameDF = { env -> + def loader = env.getDataLoader("name") + def result = loader.load("Key1").thenCompose { + return loader.load("Key1") // This should hit the cache + } + } as DataFetcher + + + def fetchers = [Query: [dog: dogDF], Dog: [name: nameDF]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ dog {name } } " + def ei = newExecutionInput(query).dataLoaderRegistry(dataLoaderRegistry).profileExecution(true).build() + setEnableDataLoaderChaining(ei.graphQLContext, true) + + when: + def efCF = graphQL.executeAsync(ei) + Awaitility.await().until { efCF.isDone() } + def er = efCF.get() + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + then: + er.data == [dog: [name: "Luna"]] + batchLoadCalls.get() == 1 + then: + profilerResult.getDataLoaderLoadInvocations().get("name") == 4 + profilerResult.getDispatchEvents()[0].type == ProfilerResult.DispatchEventType.STRATEGY_DISPATCH + profilerResult.getDispatchEvents()[0].dataLoaderName == "name" + profilerResult.getDispatchEvents()[0].keyCount == 1 + profilerResult.getDispatchEvents()[0].level == 1 + + } + + + def "collects instrumentation list"() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl, [Query: [ + hello: { DataFetchingEnvironment dfe -> return "world" } as DataFetcher + ]]) + Instrumentation fooInstrumentation = new Instrumentation() {}; + Instrumentation barInstrumentation = new Instrumentation() {}; + ChainedInstrumentation chainedInstrumentation = new ChainedInstrumentation( + new ChainedInstrumentation(new SimplePerformantInstrumentation()), + new ChainedInstrumentation(fooInstrumentation, barInstrumentation), + new SimplePerformantInstrumentation()) + + + def graphql = GraphQL.newGraphQL(schema).instrumentation(chainedInstrumentation).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ hello }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [hello: "world"] + + then: + profilerResult.getInstrumentationClasses() == ["graphql.execution.instrumentation.SimplePerformantInstrumentation", + "graphql.ProfilerTest\$2", + "graphql.ProfilerTest\$3", + "graphql.execution.instrumentation.SimplePerformantInstrumentation"] + + } + + + def "two DF with list"() { + given: + def sdl = ''' + type Query { + foo: [Foo] + } + type Foo { + id: String + bar: String + } + ''' + def schema = TestUtil.schema(sdl, [ + Query: [ + foo: { DataFetchingEnvironment dfe -> return [[id: "1"], [id: "2"], [id: "3"]] } as DataFetcher], + Foo : [ + bar: { DataFetchingEnvironment dfe -> dfe.source.id } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ foo { id bar } }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [foo: [[id: "1", bar: "1"], [id: "2", bar: "2"], [id: "3", bar: "3"]]] + + then: + profilerResult.getFieldsFetched() == ["/foo", "/foo/bar", "/foo/id"] as Set + profilerResult.getTotalDataFetcherInvocations() == 7 + profilerResult.getTotalCustomDataFetcherInvocations() == 4 + profilerResult.getTotalTrivialDataFetcherInvocations() == 3 + } + + def "records timing"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + type Foo { + id: String + } + ''' + def schema = TestUtil.schema(sdl, [ + Query: [ + foo: { DataFetchingEnvironment dfe -> + // blocking the engine for 1ms + // so that engineTotalRunningTime time is more than 1ms + Thread.sleep(1) + return CompletableFuture.supplyAsync { + Thread.sleep(500) + "1" + } + } as DataFetcher], + Foo : [ + id: { DataFetchingEnvironment dfe -> + return CompletableFuture.supplyAsync { + Thread.sleep(500) + dfe.source + } + } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ foo { id } }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [foo: [id: "1"]] + // the total execution time must be more than 1 second, + // the engine should take less than 500ms + profilerResult.getTotalExecutionTime() > Duration.ofSeconds(1).toNanos() + profilerResult.getEngineTotalRunningTime() > Duration.ofMillis(1).toNanos() + profilerResult.getEngineTotalRunningTime() < Duration.ofMillis(500).toNanos() + + + } + + def "data fetcher result types"() { + given: + def sdl = ''' + type Query { + foo: [Foo] + } + type Foo { + id: String + name: String + text: String + } + ''' + def schema = TestUtil.schema(sdl, [ + Query: [ + foo: { DataFetchingEnvironment dfe -> + return CompletableFuture.supplyAsync { + Thread.sleep(100) + return [[id: "1", name: "foo1"], [id: "2", name: "foo2"]] + } + } as DataFetcher], + Foo : [ + name: { DataFetchingEnvironment dfe -> + return CompletableFuture.completedFuture(dfe.source.name) + } as DataFetcher, + text: { DataFetchingEnvironment dfe -> + return "text" + } as DataFetcher + + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("{ foo { id name text } foo2: foo { id name text} }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [foo: [[id: "1", name: "foo1", text: "text"], [id: "2", name: "foo2", text: "text"]], foo2: [[id: "1", name: "foo1", text: "text"], [id: "2", name: "foo2", text: "text"]]] + then: + profilerResult.getTotalDataFetcherInvocations() == 14 + profilerResult.getTotalCustomDataFetcherInvocations() == 10 + profilerResult.getDataFetcherResultType() == ["/foo/name" : COMPLETABLE_FUTURE_COMPLETED, + "/foo/text" : MATERIALIZED, + "/foo2/name": COMPLETABLE_FUTURE_COMPLETED, + "/foo2/text": MATERIALIZED, + "/foo2" : COMPLETABLE_FUTURE_NOT_COMPLETED, + "/foo" : COMPLETABLE_FUTURE_NOT_COMPLETED] + profilerResult.shortSummaryMap().get("dataFetcherResultTypes") == ["COMPLETABLE_FUTURE_COMPLETED" : "(count:2, invocations:4)", + "COMPLETABLE_FUTURE_NOT_COMPLETED": "(count:2, invocations:2)", + "MATERIALIZED" : "(count:2, invocations:4)"] + + + } + + def "operation details"() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl, [Query: [ + hello: { DataFetchingEnvironment dfe -> return "world" } as DataFetcher + ]]) + def graphql = GraphQL.newGraphQL(schema).build(); + + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("query MyQuery { hello }") + .profileExecution(true) + .build() + + when: + def result = graphql.execute(ei) + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + + then: + result.getData() == [hello: "world"] + + then: + profilerResult.getOperationName() == "MyQuery" + profilerResult.getOperationType() == OperationDefinition.Operation.QUERY + + + } + + def "dataloader usage"() { + given: + def sdl = ''' + + type Query { + dogName: String + catName: String + } + ''' + int batchLoadCalls = 0 + BatchLoader batchLoader = { keys -> + return supplyAsync { + batchLoadCalls++ + Thread.sleep(250) + println "BatchLoader called with keys: $keys" + assert keys.size() == 2 + return ["Luna", "Tiger"] + } + } + + DataLoader nameDataLoader = DataLoaderFactory.newDataLoader(batchLoader); + + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + dataLoaderRegistry.register("name", nameDataLoader); + + def df1 = { env -> + return env.getDataLoader("name").load("Key1").thenCompose { + result -> + { + return env.getDataLoader("name").load(result) + } + } + } as DataFetcher + + def df2 = { env -> + return env.getDataLoader("name").load("Key2").thenCompose { + result -> + { + return env.getDataLoader("name").load(result) + } + } + } as DataFetcher + + def fetchers = ["Query": ["dogName": df1, "catName": df2]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ dogName catName } " + def ei = newExecutionInput(query).dataLoaderRegistry(dataLoaderRegistry).profileExecution(true).build() + setEnableDataLoaderChaining(ei.graphQLContext, true) + + when: + def efCF = graphQL.executeAsync(ei) + Awaitility.await().until { efCF.isDone() } + def er = efCF.get() + def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult + then: + er.data == [dogName: "Luna", catName: "Tiger"] + batchLoadCalls == 2 + profilerResult.isDataLoaderChainingEnabled() + profilerResult.getDataLoaderLoadInvocations() == [name: 4] + profilerResult.getDispatchEvents().size() == 2 + profilerResult.getDispatchEvents()[0].dataLoaderName == "name" + profilerResult.getDispatchEvents()[0].level == 1 + profilerResult.getDispatchEvents()[0].keyCount == 2 + profilerResult.getDispatchEvents()[1].dataLoaderName == "name" + profilerResult.getDispatchEvents()[1].level == 1 + profilerResult.getDispatchEvents()[1].keyCount == 2 + + } + + +} diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index 18c35d260d..0c66a30e18 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -5,6 +5,7 @@ import graphql.ErrorType import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQLContext +import graphql.Profiler import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimplePerformantInstrumentation @@ -112,7 +113,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .graphQLContext(graphqlContextMock) .executionInput(ei) .locale(Locale.getDefault()) - .engineRunningState(new EngineRunningState(ei)) + .profiler(Profiler.NO_OP) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -157,7 +159,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .locale(Locale.getDefault()) .graphQLContext(graphqlContextMock) .executionInput(ei) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) + .profiler(Profiler.NO_OP) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -203,8 +206,9 @@ abstract class AsyncExecutionStrategyTest extends Specification { .instrumentation(SimplePerformantInstrumentation.INSTANCE) .graphQLContext(graphqlContextMock) .executionInput(ei) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) .locale(Locale.getDefault()) + .profiler(Profiler.NO_OP) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -250,7 +254,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .locale(Locale.getDefault()) .graphQLContext(graphqlContextMock) .executionInput(ei) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) + .profiler(Profiler.NO_OP) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -294,7 +299,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .graphQLContext(graphqlContextMock) .executionInput(ei) .locale(Locale.getDefault()) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) + .profiler(Profiler.NO_OP) .instrumentation(new SimplePerformantInstrumentation() { @Override diff --git a/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy index ba1f83e189..6089e75a88 100644 --- a/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy @@ -3,6 +3,7 @@ package graphql.execution import graphql.EngineRunningState import graphql.ExecutionInput import graphql.GraphQLContext +import graphql.Profiler import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.language.Field import graphql.language.OperationDefinition @@ -109,8 +110,10 @@ class AsyncSerialExecutionStrategyTest extends Specification { .valueUnboxer(ValueUnboxer.DEFAULT) .locale(Locale.getDefault()) .graphQLContext(GraphQLContext.getDefault()) + .executionInput(ExecutionInput.newExecutionInput("{}").build()) + .profiler(Profiler.NO_OP) .executionInput(ei) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -160,7 +163,9 @@ class AsyncSerialExecutionStrategyTest extends Specification { .locale(Locale.getDefault()) .graphQLContext(GraphQLContext.getDefault()) .executionInput(ei) - .engineRunningState(new EngineRunningState(ei)) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) + .executionInput(ExecutionInput.newExecutionInput("{}").build()) + .profiler(Profiler.NO_OP) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy index 81df0165af..71eee4f284 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy @@ -7,6 +7,7 @@ import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQLContext import graphql.GraphqlErrorBuilder +import graphql.Profiler import graphql.Scalars import graphql.SerializationError import graphql.StarWarsSchema @@ -87,7 +88,8 @@ class ExecutionStrategyTest extends Specification { .dataLoaderRegistry(new DataLoaderRegistry()) .locale(Locale.getDefault()) .valueUnboxer(ValueUnboxer.DEFAULT) - .engineRunningState(new EngineRunningState(ei)) + .profiler(Profiler.NO_OP) + .engineRunningState(new EngineRunningState(ei, Profiler.NO_OP)) new ExecutionContext(builder) } diff --git a/src/test/groovy/graphql/execution/ExecutionTest.groovy b/src/test/groovy/graphql/execution/ExecutionTest.groovy index 6d207ae1a1..46ac3c7942 100644 --- a/src/test/groovy/graphql/execution/ExecutionTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionTest.groovy @@ -5,6 +5,7 @@ import graphql.ExecutionInput import graphql.ExecutionResult import graphql.ExecutionResultImpl import graphql.MutationSchema +import graphql.Profiler import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters @@ -52,7 +53,7 @@ class ExecutionTest extends Specification { def document = parser.parseDocument(query) when: - execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput)) + execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP) then: queryStrategy.execute == 1 @@ -72,7 +73,7 @@ class ExecutionTest extends Specification { def document = parser.parseDocument(query) when: - execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput)) + execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP) then: queryStrategy.execute == 0 @@ -92,7 +93,7 @@ class ExecutionTest extends Specification { def document = parser.parseDocument(query) when: - execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput)) + execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP) then: queryStrategy.execute == 0 @@ -129,7 +130,7 @@ class ExecutionTest extends Specification { when: - execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput)) + execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP) then: queryStrategy.execute == 0 diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy index 27e820750f..2aaad6090a 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy @@ -56,15 +56,9 @@ class DataLoaderDispatcherTest extends Specification { ] - def "dispatch is called if there are data loaders"() { + def "basic dataloader dispatch test"() { def dispatchedCalled = false - def dataLoaderRegistry = new DataLoaderRegistry() { - @Override - void dispatchAll() { - dispatchedCalled = true - super.dispatchAll() - } - } + def dataLoaderRegistry = new DataLoaderRegistry() def dataLoader = DataLoaderFactory.newDataLoader(new BatchLoader() { @Override CompletionStage load(List keys) { @@ -78,10 +72,11 @@ class DataLoaderDispatcherTest extends Specification { executionInput.getGraphQLContext().put(DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_CHAINING, false) when: - def er = graphQL.execute(executionInput) + def er = graphQL.executeAsync(executionInput) + Awaitility.await().until { er.isDone() } then: - er.errors.isEmpty() - dispatchedCalled + er.get().data == [hero: [name: 'R2-D2']] + } def "enhanced execution input is respected"() { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DeferWithDataLoaderTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DeferWithDataLoaderTest.groovy index 5427f7e504..2978d31c91 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DeferWithDataLoaderTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DeferWithDataLoaderTest.groovy @@ -11,6 +11,7 @@ import org.awaitility.Awaitility import org.dataloader.BatchLoader import org.dataloader.DataLoaderFactory import org.dataloader.DataLoaderRegistry +import spock.lang.RepeatUntilFailure import spock.lang.Specification import java.time.Duration @@ -348,6 +349,7 @@ class DeferWithDataLoaderTest extends Specification { batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 } + @RepeatUntilFailure(maxAttempts = 50) def "dataloader in initial result and chained dataloader inside nested defer block"() { given: def sdl = ''' diff --git a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy index 2852d262b4..fc0ae070e6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy @@ -5,12 +5,12 @@ import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL import graphql.GraphQLError +import graphql.Profiler import graphql.TestUtil import graphql.execution.AbortExecutionException import graphql.execution.AsyncExecutionStrategy import graphql.execution.Execution import graphql.execution.ExecutionId -import graphql.execution.ResponseMapFactory import graphql.execution.ResultPath import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation @@ -310,7 +310,7 @@ class FieldValidationTest extends Specification { def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT, false) def executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build() - execution.execute(document, schema, ExecutionId.generate(), executionInput, null, new EngineRunningState(executionInput)) + execution.execute(document, schema, ExecutionId.generate(), executionInput, null, new EngineRunningState(executionInput, Profiler.NO_OP), Profiler.NO_OP) } def "test graphql from end to end with chained instrumentation"() {