From cd9cb861c0acc9f4cf2595abb1a6a987a6c83be2 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:53:35 +1100 Subject: [PATCH 1/4] gnhf #2: Added field collection caching on ExecutionContext, deferred CompletableFuture allocation in executeObject's sync path, and cached max result nodes lookup to eliminate ~30M+ unnecessary object allocations per 10M-element list execution. --- .../graphql/execution/ExecutionContext.java | 43 +++++++++++++++++++ .../graphql/execution/ExecutionStrategy.java | 31 ++++++------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 51a72b56dc..a5fcfdbb96 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -31,10 +31,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 +78,12 @@ 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<>(); private final EngineRunningState engineRunningState; private final Supplier>> allOperationsDirectives; @@ -108,6 +117,7 @@ public class ExecutionContext { this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; this.profiler = builder.profiler; + this.maxResultNodes = builder.graphQLContext != null ? builder.graphQLContext.get(ResultNodesInfo.MAX_RESULT_NODES) : null; // lazy loading for performance this.queryTree = mkExecutableNormalizedOperation(); this.allOperationsDirectives = builder.allOperationsDirectives; @@ -406,12 +416,45 @@ 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); } + /** + * 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 public EngineRunningState getEngineRunningState() { return engineRunningState; diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 9f402a24d6..636dc7c9e8 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; @@ -212,13 +210,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); @@ -252,11 +251,16 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat 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; @@ -943,16 +947,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 +1006,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; From ed0d65bf0e7b9ae553bea3d580160d9e7049c5af Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:02:10 +1100 Subject: [PATCH 2/4] gnhf #3: Added no-op instrumentation fast path to skip ~40M+ unnecessary instrumentation parameter object allocations in executeObject, resolveFieldWithInfo, fetchField, and completeField when no custom instrumentation is configured (the default). --- .../graphql/execution/ExecutionContext.java | 14 ++++ .../graphql/execution/ExecutionStrategy.java | 72 ++++++++++++++----- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index a5fcfdbb96..db3ceb5535 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; @@ -84,6 +85,9 @@ public class ExecutionContext { // 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; private final EngineRunningState engineRunningState; private final Supplier>> allOperationsDirectives; @@ -117,6 +121,7 @@ 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; // lazy loading for performance this.queryTree = mkExecutableNormalizedOperation(); @@ -154,6 +159,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; } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 636dc7c9e8..fd0ea55e77 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -196,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(); @@ -364,6 +369,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(); @@ -469,14 +495,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(); @@ -649,18 +681,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()) { From d3bbf75a85d5c589ee152c2969c5efa5b79f3125 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:19:36 +1100 Subject: [PATCH 3/4] gnhf #4: Cached hasIncrementalSupport() result on ExecutionContext to eliminate ~20M+ ConcurrentHashMap lookups per 10M-element list execution, and added no-op instrumentation fast path in completeValueForList to skip unnecessary InstrumentationFieldCompleteParameters allocation and instrumentation callbacks. --- .../graphql/execution/ExecutionContext.java | 6 +++-- .../graphql/execution/ExecutionStrategy.java | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index db3ceb5535..73e6c66f61 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -88,6 +88,8 @@ public class ExecutionContext { // 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; @@ -123,6 +125,7 @@ public class ExecutionContext { 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; @@ -442,8 +445,7 @@ public Integer getMaxResultNodes() { @Internal public boolean hasIncrementalSupport() { - GraphQLContext graphqlContext = getGraphQLContext(); - return graphqlContext != null && graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT); + return incrementalSupport; } /** diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index fd0ea55e77..b4dce08a30 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -834,12 +834,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; @@ -871,8 +876,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); @@ -887,7 +894,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); From fdb5ca384de292cf28907910640f97f61052e731 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:28:49 +1100 Subject: [PATCH 4/4] gnhf #5: Bypassed Builder pattern in ExecutionStepInfoFactory.createExecutionStepInfo and added single-field fast path in executeObject's sync path to eliminate ~30M unnecessary object allocations per 10M-element list execution. --- .../graphql/execution/ExecutionStepInfo.java | 14 +++++++++++++ .../execution/ExecutionStepInfoFactory.java | 21 ++++++++----------- .../graphql/execution/ExecutionStrategy.java | 15 ++++++++++++- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 26737cd127..de534cc0ce 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 286106a7dc..07c6a81612 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 b4dce08a30..31e2d270c5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -250,10 +250,23 @@ 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