diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 51a72b56d..73e6c66f6 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -16,6 +16,7 @@ import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.language.Document; import graphql.language.FragmentDefinition; import graphql.language.OperationDefinition; @@ -31,10 +32,13 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; +import graphql.schema.GraphQLObjectType; + import static graphql.normalized.ExecutableNormalizedOperationFactory.Options; import static graphql.normalized.ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation; @@ -75,6 +79,17 @@ public class ExecutionContext { private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); + // Cached max result nodes limit - avoids ConcurrentHashMap lookup on every node increment + private final Integer maxResultNodes; + // Per-execution cache for field collection results. Within a single execution, the collected fields + // for a given (objectType, mergedField) pair are always the same since schema, fragments, variables + // and graphQLContext are constant. This avoids recomputing field collection for every element in large lists. + private final ConcurrentHashMap> fieldCollectionCache = new ConcurrentHashMap<>(); + // True when instrumentation is the no-op singleton, allowing hot paths to skip + // instrumentation parameter object allocations entirely + private final boolean noOpInstrumentation; + // Cached incremental support flag - avoids ConcurrentHashMap lookup on every call + private final boolean incrementalSupport; private final EngineRunningState engineRunningState; private final Supplier>> allOperationsDirectives; @@ -108,6 +123,9 @@ public class ExecutionContext { this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; this.profiler = builder.profiler; + this.noOpInstrumentation = builder.instrumentation == SimplePerformantInstrumentation.INSTANCE; + this.maxResultNodes = builder.graphQLContext != null ? builder.graphQLContext.get(ResultNodesInfo.MAX_RESULT_NODES) : null; + this.incrementalSupport = builder.graphQLContext != null && builder.graphQLContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT); // lazy loading for performance this.queryTree = mkExecutableNormalizedOperation(); this.allOperationsDirectives = builder.allOperationsDirectives; @@ -144,6 +162,15 @@ public Instrumentation getInstrumentation() { return instrumentation; } + /** + * @return true if the instrumentation is the no-op singleton, meaning instrumentation + * parameter objects don't need to be allocated in hot paths + */ + @Internal + public boolean isNoOpInstrumentation() { + return noOpInstrumentation; + } + public GraphQLSchema getGraphQLSchema() { return graphQLSchema; } @@ -406,10 +433,42 @@ public ResultNodesInfo getResultNodesInfo() { return resultNodesInfo; } + /** + * Returns the cached max result nodes limit, avoiding repeated ConcurrentHashMap lookups. + * + * @return the max result nodes limit, or null if not set + */ + @Internal + public Integer getMaxResultNodes() { + return maxResultNodes; + } + @Internal public boolean hasIncrementalSupport() { - GraphQLContext graphqlContext = getGraphQLContext(); - return graphqlContext != null && graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT); + return incrementalSupport; + } + + /** + * Returns cached field collection results for the given object type and merged field, computing them + * if not already cached. Within a single execution, field collection results are deterministic for + * the same (objectType, mergedField) pair. + */ + @Internal + public MergedSelectionSet getOrComputeFieldCollection(FieldCollector fieldCollector, + GraphQLObjectType objectType, + MergedField mergedField, + boolean incrementalSupport) { + ConcurrentHashMap innerCache = fieldCollectionCache.computeIfAbsent(objectType, k -> new ConcurrentHashMap<>()); + return innerCache.computeIfAbsent(mergedField, k -> { + FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() + .schema(graphQLSchema) + .objectType(objectType) + .fragments(fragmentsByName) + .variables(coercedVariables.toMap()) + .graphQLContext(graphQLContext) + .build(); + return fieldCollector.collectFields(collectorParameters, mergedField, incrementalSupport); + }); } @Internal diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 26737cd12..de534cc0c 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -278,6 +278,20 @@ public static ExecutionStepInfo.Builder newExecutionStepInfo(ExecutionStepInfo e return new Builder(existing); } + /** + * Direct construction without Builder overhead - for internal hot path use only. + */ + @Internal + static ExecutionStepInfo newExecutionStepInfoDirect(GraphQLOutputType type, + ResultPath path, + ExecutionStepInfo parent, + MergedField field, + GraphQLFieldDefinition fieldDefinition, + GraphQLObjectType fieldContainer, + Supplier> arguments) { + return new ExecutionStepInfo(type, path, parent, field, fieldDefinition, fieldContainer, arguments); + } + public static class Builder { GraphQLOutputType type; ExecutionStepInfo parentInfo; diff --git a/src/main/java/graphql/execution/ExecutionStepInfoFactory.java b/src/main/java/graphql/execution/ExecutionStepInfoFactory.java index 286106a7d..07c6a8161 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfoFactory.java +++ b/src/main/java/graphql/execution/ExecutionStepInfoFactory.java @@ -18,8 +18,6 @@ import java.util.Map; import java.util.function.Supplier; -import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; - @Internal @NullMarked public class ExecutionStepInfoFactory { @@ -56,16 +54,15 @@ public ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionConte argumentValues = getArgumentValues(executionContext, fieldArgDefs, field.getArguments()); } - - return newExecutionStepInfo() - .type(fieldType) - .fieldDefinition(fieldDefinition) - .fieldContainer(fieldContainer) - .field(field) - .path(parameters.getPath()) - .parentInfo(parentStepInfo) - .arguments(argumentValues) - .build(); + // Direct construction bypasses Builder allocation + return ExecutionStepInfo.newExecutionStepInfoDirect( + fieldType, + parameters.getPath(), + parentStepInfo, + field, + fieldDefinition, + fieldContainer, + argumentValues); } @NonNull diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 9f402a24d..31e2d270c 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -58,13 +58,11 @@ import java.util.function.Supplier; import static graphql.execution.Async.exceptionallyCompletedFuture; -import static graphql.execution.FieldCollectorParameters.newParameters; import static graphql.execution.FieldValueInfo.CompleteValueType.ENUM; import static graphql.execution.FieldValueInfo.CompleteValueType.LIST; import static graphql.execution.FieldValueInfo.CompleteValueType.NULL; import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT; import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR; -import static graphql.execution.ResultNodesInfo.MAX_RESULT_NODES; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment; import static graphql.schema.GraphQLTypeUtil.isEnum; @@ -198,12 +196,17 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat executionContext.throwIfCancelled(); DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy(); - Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); - ExecuteObjectInstrumentationContext resolveObjectCtx = ExecuteObjectInstrumentationContext.nonNullCtx( - instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) - ); + ExecuteObjectInstrumentationContext resolveObjectCtx; + if (executionContext.isNoOpInstrumentation()) { + resolveObjectCtx = ExecuteObjectInstrumentationContext.NOOP; + } else { + Instrumentation instrumentation = executionContext.getInstrumentation(); + InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); + resolveObjectCtx = ExecuteObjectInstrumentationContext.nonNullCtx( + instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) + ); + } List fieldNames = parameters.getFields().getKeys(); @@ -212,13 +215,14 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat dataLoaderDispatcherStrategy.executeObject(executionContext, parameters, fieldsExecutedOnInitialResult.size()); Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); - CompletableFuture> overallResult = new CompletableFuture<>(); - BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult, executionContext); - resolveObjectCtx.onDispatched(); Object fieldValueInfosResult = resolvedFieldFutures.awaitPolymorphic(); if (fieldValueInfosResult instanceof CompletableFuture) { + // Async path - allocate CF infrastructure only when needed + CompletableFuture> overallResult = new CompletableFuture<>(); + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult, executionContext); + CompletableFuture> fieldValueInfos = (CompletableFuture>) fieldValueInfosResult; fieldValueInfos.whenComplete((completeValueInfos, throwable) -> { throwable = executionContext.possibleCancellation(throwable); @@ -246,17 +250,35 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat } else { List completeValueInfos = (List) fieldValueInfosResult; - Async.CombinedBuilder resultFutures = fieldValuesCombinedBuilder(completeValueInfos); dataLoaderDispatcherStrategy.executeObjectOnFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); + // Fast path: single sync field value - avoid CombinedBuilder overhead + if (completeValueInfos.size() == 1) { + FieldValueInfo singleFvi = completeValueInfos.get(0); + Object fieldValue = singleFvi.getFieldValueObject(); + if (!(fieldValue instanceof CompletableFuture)) { + Map fieldValueMap = executionContext.getResponseMapFactory() + .createInsertionOrdered(fieldsExecutedOnInitialResult, Collections.singletonList(fieldValue)); + resolveObjectCtx.onCompleted(fieldValueMap, null); + return fieldValueMap; + } + } + + Async.CombinedBuilder resultFutures = fieldValuesCombinedBuilder(completeValueInfos); + Object completedValuesObject = resultFutures.awaitPolymorphic(); if (completedValuesObject instanceof CompletableFuture) { + // Semi-async path - allocate CF infrastructure only when needed + CompletableFuture> overallResult = new CompletableFuture<>(); + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult, executionContext); + CompletableFuture> completedValues = (CompletableFuture>) completedValuesObject; completedValues.whenComplete(handleResultsConsumer); overallResult.whenComplete(resolveObjectCtx::onCompleted); return overallResult; } else { + // Fully sync path - no CompletableFuture allocation needed Map fieldValueMap = executionContext.getResponseMapFactory().createInsertionOrdered(fieldsExecutedOnInitialResult, (List) completedValuesObject); resolveObjectCtx.onCompleted(fieldValueMap, null); return fieldValueMap; @@ -360,6 +382,27 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi @DuckTyped(shape = "CompletableFuture | FieldValueInfo") protected Object resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); + + if (executionContext.isNoOpInstrumentation()) { + // Fast path: skip instrumentation parameter allocation and context creation + Object fetchedValueObj = fetchField(fieldDef, executionContext, parameters); + if (fetchedValueObj instanceof CompletableFuture) { + CompletableFuture fetchFieldFuture = (CompletableFuture) fetchedValueObj; + return fetchFieldFuture.thenApply((fetchedValue) -> { + executionContext.getDataLoaderDispatcherStrategy().startComplete(parameters); + FieldValueInfo completeFieldResult = completeField(fieldDef, executionContext, parameters, fetchedValue); + executionContext.getDataLoaderDispatcherStrategy().stopComplete(parameters); + return completeFieldResult; + }); + } else { + try { + return completeField(fieldDef, executionContext, parameters, fetchedValueObj); + } catch (Exception e) { + return Async.exceptionallyCompletedFuture(e); + } + } + } + Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -465,14 +508,20 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); DataFetcher originalDataFetcher = codeRegistry.getDataFetcher(parentType.getName(), fieldDef.getName(), fieldDef); - Instrumentation instrumentation = executionContext.getInstrumentation(); - - InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, originalDataFetcher instanceof TrivialDataFetcher); - FieldFetchingInstrumentationContext fetchCtx = FieldFetchingInstrumentationContext.nonNullCtx(instrumentation.beginFieldFetching(instrumentationFieldFetchParams, - executionContext.getInstrumentationState()) - ); + DataFetcher dataFetcher; + FieldFetchingInstrumentationContext fetchCtx; + if (executionContext.isNoOpInstrumentation()) { + dataFetcher = originalDataFetcher; + fetchCtx = FieldFetchingInstrumentationContext.NOOP; + } else { + Instrumentation instrumentation = executionContext.getInstrumentation(); + InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, originalDataFetcher instanceof TrivialDataFetcher); + fetchCtx = FieldFetchingInstrumentationContext.nonNullCtx(instrumentation.beginFieldFetching(instrumentationFieldFetchParams, + executionContext.getInstrumentationState()) + ); + dataFetcher = instrumentation.instrumentDataFetcher(originalDataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); + } - 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(); @@ -645,18 +694,22 @@ private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionC GraphQLObjectType parentType = parameters.getExecutionStepInfo().getUnwrappedNonNullTypeAs(); ExecutionStepInfo executionStepInfo = createExecutionStepInfo(executionContext, parameters, fieldDef, parentType); - Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); - InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldCompletion( - instrumentationParams, executionContext.getInstrumentationState() - )); - ExecutionStrategyParameters newParameters = parameters.transform( executionStepInfo, FetchedValue.getLocalContext(fetchedValue, parameters.getLocalContext()), FetchedValue.getFetchedValue(fetchedValue) ); + if (executionContext.isNoOpInstrumentation()) { + return completeValue(executionContext, newParameters); + } + + Instrumentation instrumentation = executionContext.getInstrumentation(); + InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); + InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldCompletion( + instrumentationParams, executionContext.getInstrumentationState() + )); + FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); ctxCompleteField.onDispatched(); if (fieldValueInfo.isFutureValue()) { @@ -794,12 +847,17 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, OptionalInt size = FpKit.toSize(iterableValues); ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); - InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, iterableValues); - Instrumentation instrumentation = executionContext.getInstrumentation(); - - InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListCompletion( - instrumentationParams, executionContext.getInstrumentationState() - )); + boolean noOpInstrumentation = executionContext.isNoOpInstrumentation(); + InstrumentationContext completeListCtx; + if (noOpInstrumentation) { + completeListCtx = null; + } else { + InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, iterableValues); + Instrumentation instrumentation = executionContext.getInstrumentation(); + completeListCtx = nonNullCtx(instrumentation.beginFieldListCompletion( + instrumentationParams, executionContext.getInstrumentationState() + )); + } List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; @@ -831,8 +889,10 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, @SuppressWarnings("unchecked") CompletableFuture> resultsFuture = (CompletableFuture>) listResults; CompletableFuture overallResult = new CompletableFuture<>(); - completeListCtx.onDispatched(); - overallResult.whenComplete(completeListCtx::onCompleted); + if (completeListCtx != null) { + completeListCtx.onDispatched(); + overallResult.whenComplete(completeListCtx::onCompleted); + } resultsFuture.whenComplete((results, exception) -> { exception = executionContext.possibleCancellation(exception); @@ -847,7 +907,9 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, }); listOrPromiseToList = overallResult; } else { - completeListCtx.onCompleted(listResults, null); + if (completeListCtx != null) { + completeListCtx.onCompleted(listResults, null); + } listOrPromiseToList = listResults; } return new FieldValueInfo(LIST, listOrPromiseToList, fieldValueInfos); @@ -943,16 +1005,9 @@ protected Object completeValueForEnum(ExecutionContext executionContext, Executi protected Object completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); - FieldCollectorParameters collectorParameters = newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(resolvedObjectType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getCoercedVariables().toMap()) - .graphQLContext(executionContext.getGraphQLContext()) - .build(); - - MergedSelectionSet subFields = fieldCollector.collectFields( - collectorParameters, + MergedSelectionSet subFields = executionContext.getOrComputeFieldCollection( + fieldCollector, + resolvedObjectType, parameters.getField(), executionContext.hasIncrementalSupport() ); @@ -1009,8 +1064,8 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate */ private boolean incrementAndCheckMaxNodesExceeded(ExecutionContext executionContext) { int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); - Integer maxNodes; - if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { + Integer maxNodes = executionContext.getMaxResultNodes(); + if (maxNodes != null) { if (resultNodesCount > maxNodes) { executionContext.getResultNodesInfo().maxResultNodesExceeded(); return true;