Repository: graphql-java/graphql-java (language: java)

Issue:
Some CPU overhead from ChainedInstrumentation
**Describe the bug**

We register more than one instrumentation as part of our DGS framework, which leads to `ChainedInstrumentation` being used. This class shows as a hotspot for field instrumentation in particular. In the case of the application I was looking at, I'd estimate it's 5-8% of the CPU overhead while satisfying queries:

<img width="1174" alt="Screenshot 2024-02-05 at 8 53 12 pm" src="https://github.com/graphql-java/graphql-java/assets/1479220/d58f245b-6e40-4392-8722-572e7606a0cb">

Data fetching for this service is all GRPC/Netty/EVCache, so `graphql-java` hotspots become more visible that they might otherwise be; this is very likely the worst case for this class.

**To Reproduce**

Note the overhead of field instrumentation when using chained instrumentations, due to the frequency of `beginField` calls for any non-trivial query.


Discussion / hints:
We use this workaround

```kotlin
/**
 * ChainedInstrumentation is slow, so we use this class to more manually combine multiple instrumentations into one.
 */
class FastGraphQLInstrumentations(
    private val searchQueryCacheTTLInstrumentation: SearchQueryCacheTTLInstrumentation,
    private val disableCacheInstrumentation: DisableCacheInstrumentation,
    private val modifyHttpStatusCodeInstrumentation: ModifyHttpStatusCodeInstrumentation
) : SimplePerformantInstrumentation() {

    override fun instrumentExecutionResult(
        executionResult: ExecutionResult?,
        parameters: InstrumentationExecutionParameters?,
        state: InstrumentationState?
    ): CompletableFuture<ExecutionResult> {
        val queryContext = parameters?.graphQLContext?.get<QueryContext>("queryContext")
        if (queryContext != null && executionResult != null) {
            val result = modifyHttpStatusCodeInstrumentation.modifyHttpStatusCode(queryContext, executionResult)
            if (result != null) {
                return result
            }
        }
        if (queryContext != null && executionResult != null) {
            val result = searchQueryCacheTTLInstrumentation.addCacheTTLForShortSearchQueries(queryContext, executionResult)
            if (result != null) {
                return result
            }
        }
        if (queryContext != null && executionResult != null) {
            val result = disableCacheInstrumentation.disableCaching(queryContext, executionResult)
            if (result != null) {
                return result
            }
        }
        return super.instrumentExecutionResult(executionResult, parameters, state)
    }

}
```
> This class shows as a hotspot for field instrumentation in particular.

> Note the overhead of field instrumentation when using chained instrumentations, due to the frequency of beginField calls for any non-trivial query.

So in many ways this is expected.  The profiling above shows 

```
private InstrumentationState getState(Instrumentation instrumentation) {
            return instrumentationToStates.get(instrumentation);
}
```

which is a hashmap of states in the chain.  A map look up should not be slow but it is being called often.

The same is true of beginField - if you query has lots of fields - the instrumentation is going to get a callback  for each field.

You mention that you have a couple of instrumentations - what are they ?

Ideally they would directive from `graphql.execution.instrumentation.SimplePerformantInstrumentation` which is newish and a smidge more performant.

The optimization that @rrva has will be faster because its very specific.  The graphql java code needs to be able to generically run any instrumentation.  Where as @rrva runs specific bits of code that he is aware of - which is a faster way to run things but its not generic - its highly coupled to specific code

That said the next version of graphql-java is trying to reduce the default data loader instrumentation that always gets added.  Instead if will be implemented in the engine itself.  This MAY save some time but we havent yet done the perf tests for that.


Fix patch:
diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java
index 35847dfdde..b1b5eb2dbb 100644
--- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java
+++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java
@@ -1,7 +1,6 @@
 package graphql.execution.instrumentation;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Maps;
 import graphql.Assert;
 import graphql.ExecutionInput;
 import graphql.ExecutionResult;
@@ -25,16 +24,16 @@
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.AbstractMap;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 
 import static graphql.Assert.assertNotNull;
-import static graphql.collect.ImmutableKit.mapAndDropNulls;
 
 /**
  * This allows you to chain together a number of {@link graphql.execution.instrumentation.Instrumentation} implementations
@@ -67,21 +66,48 @@ public List<Instrumentation> getInstrumentations() {
         return instrumentations;
     }
 
-    protected InstrumentationState getSpecificState(Instrumentation instrumentation, InstrumentationState parametersInstrumentationState) {
-        ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) parametersInstrumentationState;
-        return chainedInstrumentationState.getState(instrumentation);
-    }
-
-    private <T> InstrumentationContext<T> chainedCtx(Function<Instrumentation, InstrumentationContext<T>> mapper) {
+    private <T> InstrumentationContext<T> chainedCtx(InstrumentationState state, BiFunction<Instrumentation, InstrumentationState, InstrumentationContext<T>> mapper) {
         // if we have zero or 1 instrumentations (and 1 is the most common), then we can avoid an object allocation
         // of the ChainedInstrumentationContext since it won't be needed
         if (instrumentations.isEmpty()) {
             return SimpleInstrumentationContext.noOp();
         }
+        ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state;
         if (instrumentations.size() == 1) {
-            return mapper.apply(instrumentations.get(0));
+            return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0));
+        }
+        return new ChainedInstrumentationContext<>(chainedMapAndDropNulls(chainedInstrumentationState, mapper));
+    }
+
+    private <T> T chainedInstrument(InstrumentationState state, T input, ChainedInstrumentationFunction<Instrumentation, InstrumentationState, T, T> mapper) {
+        ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state;
+        for (int i = 0; i < instrumentations.size(); i++) {
+            Instrumentation instrumentation = instrumentations.get(i);
+            InstrumentationState specificState = chainedInstrumentationState.getState(i);
+            input = mapper.apply(instrumentation, specificState, input);
+        }
+        return input;
+    }
+
+    protected <T> ImmutableList<T> chainedMapAndDropNulls(InstrumentationState state, BiFunction<Instrumentation, InstrumentationState, T> mapper) {
+        ImmutableList.Builder<T> result = ImmutableList.builderWithExpectedSize(instrumentations.size());
+        for (int i = 0; i < instrumentations.size(); i++) {
+            Instrumentation instrumentation = instrumentations.get(i);
+            InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i);
+            T value = mapper.apply(instrumentation, specificState);
+            if (value != null) {
+                result.add(value);
+            }
+        }
+        return result.build();
+    }
+
+    protected <T> void chainedConsume(InstrumentationState state, BiConsumer<Instrumentation, InstrumentationState> stateConsumer) {
+        for (int i = 0; i < instrumentations.size(); i++) {
+            Instrumentation instrumentation = instrumentations.get(i);
+            InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i);
+            stateConsumer.accept(instrumentation, specificState);
         }
-        return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, mapper));
     }
 
     @Override
@@ -109,10 +135,7 @@ public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExe
 
     @Override
     public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginExecution(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecution(parameters, specificState));
     }
 
     @Override
@@ -123,10 +146,7 @@ public InstrumentationContext<Document> beginParse(InstrumentationExecutionParam
 
     @Override
     public InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginParse(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginParse(parameters, specificState));
     }
 
     @Override
@@ -137,10 +157,7 @@ public InstrumentationContext<List<ValidationError>> beginValidation(Instrumenta
 
     @Override
     public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginValidation(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginValidation(parameters, specificState));
     }
 
     @Override
@@ -151,10 +168,7 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrumenta
 
     @Override
     public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginExecuteOperation(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState));
     }
 
     @Override
@@ -168,14 +182,12 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument
         if (instrumentations.isEmpty()) {
             return ExecutionStrategyInstrumentationContext.NOOP;
         }
-        Function<Instrumentation, ExecutionStrategyInstrumentationContext> mapper = instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginExecutionStrategy(parameters, specificState);
-        };
+        BiFunction<Instrumentation, InstrumentationState, ExecutionStrategyInstrumentationContext> mapper = (instrumentation, specificState) -> instrumentation.beginExecutionStrategy(parameters, specificState);
+        ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state;
         if (instrumentations.size() == 1) {
-            return mapper.apply(instrumentations.get(0));
+            return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0));
         }
-        return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper));
+        return new ChainedExecutionStrategyInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper));
     }
 
     @Override
@@ -183,25 +195,18 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument
         if (instrumentations.isEmpty()) {
             return ExecuteObjectInstrumentationContext.NOOP;
         }
-        Function<Instrumentation, ExecuteObjectInstrumentationContext> mapper = instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginExecuteObject(parameters, specificState);
-        };
-        if (instrumentations.size() == 1) {
-            return mapper.apply(instrumentations.get(0));
+        BiFunction<Instrumentation, InstrumentationState, ExecuteObjectInstrumentationContext> mapper = (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState);
+        ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state;
+         if (instrumentations.size() == 1) {
+            return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0));
         }
-        return new ChainedExecuteObjectInstrumentationContext(mapAndDropNulls(instrumentations, mapper));
+        return new ChainedExecuteObjectInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper));
     }
 
     @ExperimentalApi
     @Override
     public InstrumentationContext<Object> beginDeferredField(InstrumentationState instrumentationState) {
-        return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream()
-                .map(instrumentation -> {
-                    InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState);
-                    return instrumentation.beginDeferredField(specificState);
-                })
-                .collect(Collectors.toList()));
+        return new ChainedDeferredExecutionStrategyInstrumentationContext(chainedMapAndDropNulls(instrumentationState, Instrumentation::beginDeferredField));
     }
 
     @Override
@@ -212,10 +217,7 @@ public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(Instrum
 
     @Override
     public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginSubscribedFieldEvent(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState));
     }
 
     @Override
@@ -231,10 +233,7 @@ public InstrumentationContext<ExecutionResult> beginField(InstrumentationFieldPa
 
     @Override
     public @Nullable InstrumentationContext<Object> beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginFieldExecution(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldExecution(parameters, specificState));
     }
 
     @Override
@@ -245,10 +244,7 @@ public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchP
 
     @Override
     public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginFieldFetch(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState));
     }
 
 
@@ -265,10 +261,7 @@ public InstrumentationContext<ExecutionResult> beginFieldComplete(Instrumentatio
 
     @Override
     public @Nullable InstrumentationContext<Object> beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginFieldCompletion(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState));
     }
 
 
@@ -280,18 +273,12 @@ public InstrumentationContext<ExecutionResult> beginFieldListComplete(Instrument
 
     @Override
     public InstrumentationContext<ExecutionResult> beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginFieldListComplete(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldListComplete(parameters, specificState));
     }
 
     @Override
     public @Nullable InstrumentationContext<Object> beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) {
-        return chainedCtx(instrumentation -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            return instrumentation.beginFieldListCompletion(parameters, specificState);
-        });
+        return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldListCompletion(parameters, specificState));
     }
 
     @Override
@@ -303,14 +290,7 @@ public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, In
     @NotNull
     @Override
     public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        if (instrumentations.isEmpty()) {
-            return executionInput;
-        }
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            executionInput = instrumentation.instrumentExecutionInput(executionInput, parameters, specificState);
-        }
-        return executionInput;
+        return chainedInstrument(state, executionInput, (instrumentation, specificState, accumulator) -> instrumentation.instrumentExecutionInput(accumulator, parameters, specificState));
     }
 
     @Override
@@ -322,14 +302,8 @@ public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables
     @NotNull
     @Override
     public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        if (instrumentations.isEmpty()) {
-            return documentAndVariables;
-        }
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters, specificState);
-        }
-        return documentAndVariables;
+        return chainedInstrument(state, documentAndVariables, (instrumentation, specificState, accumulator) ->
+                instrumentation.instrumentDocumentAndVariables(accumulator, parameters, specificState));
     }
 
     @Override
@@ -341,14 +315,8 @@ public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecu
     @NotNull
     @Override
     public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        if (instrumentations.isEmpty()) {
-            return schema;
-        }
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            schema = instrumentation.instrumentSchema(schema, parameters, specificState);
-        }
-        return schema;
+        return chainedInstrument(state, schema, (instrumentation, specificState, accumulator) ->
+                instrumentation.instrumentSchema(accumulator, parameters, specificState));
     }
 
     @Override
@@ -360,14 +328,8 @@ public ExecutionContext instrumentExecutionContext(ExecutionContext executionCon
     @NotNull
     @Override
     public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        if (instrumentations.isEmpty()) {
-            return executionContext;
-        }
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, specificState);
-        }
-        return executionContext;
+        return chainedInstrument(state, executionContext, (instrumentation, specificState, accumulator) ->
+                instrumentation.instrumentExecutionContext(accumulator, parameters, specificState));
     }
 
     @Override
@@ -379,14 +341,8 @@ public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, Instrume
     @NotNull
     @Override
     public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
-        if (instrumentations.isEmpty()) {
-            return dataFetcher;
-        }
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters, specificState);
-        }
-        return dataFetcher;
+        return chainedInstrument(state, dataFetcher, (Instrumentation instrumentation, InstrumentationState specificState, DataFetcher<?> accumulator) ->
+                instrumentation.instrumentDataFetcher(accumulator, parameters, specificState));
     }
 
     @Override
@@ -398,8 +354,10 @@ public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionRes
     @NotNull
     @Override
     public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) {
-        CompletableFuture<List<ExecutionResult>> resultsFuture = Async.eachSequentially(instrumentations, (instrumentation, prevResults) -> {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
+        ImmutableList<Map.Entry<Instrumentation, InstrumentationState>> entries = chainedMapAndDropNulls(state, AbstractMap.SimpleEntry::new);
+        CompletableFuture<List<ExecutionResult>> resultsFuture = Async.eachSequentially(entries, (entry, prevResults) -> {
+            Instrumentation instrumentation = entry.getKey();
+            InstrumentationState specificState = entry.getValue();
             ExecutionResult lastResult = prevResults.size() > 0 ? prevResults.get(prevResults.size() - 1) : executionResult;
             return instrumentation.instrumentExecutionResult(lastResult, parameters, specificState);
         });
@@ -407,16 +365,14 @@ public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionRes
     }
 
     static class ChainedInstrumentationState implements InstrumentationState {
-        private final Map<Instrumentation, InstrumentationState> instrumentationToStates;
+        private final List<InstrumentationState> instrumentationStates;
 
+        private ChainedInstrumentationState(List<InstrumentationState> instrumentationStates) {
+            this.instrumentationStates = instrumentationStates;
+        }
 
-        private ChainedInstrumentationState(List<Instrumentation> instrumentations, List<InstrumentationState> instrumentationStates) {
-            instrumentationToStates = Maps.newLinkedHashMapWithExpectedSize(instrumentations.size());
-            for (int i = 0; i < instrumentations.size(); i++) {
-                Instrumentation instrumentation = instrumentations.get(i);
-                InstrumentationState instrumentationState = instrumentationStates.get(i);
-                instrumentationToStates.put(instrumentation, instrumentationState);
-            }
+        private InstrumentationState getState(int index) {
+            return instrumentationStates.get(index);
         }
 
         private static CompletableFuture<InstrumentationState> combineAll(List<Instrumentation> instrumentations, InstrumentationCreateStateParameters parameters) {
@@ -426,11 +382,7 @@ private static CompletableFuture<InstrumentationState> combineAll(List<Instrumen
                 CompletableFuture<InstrumentationState> stateCF = Async.orNullCompletedFuture(instrumentation.createStateAsync(parameters));
                 builder.add(stateCF);
             }
-            return builder.await().thenApply(instrumentationStates -> new ChainedInstrumentationState(instrumentations, instrumentationStates));
-        }
-
-        private InstrumentationState getState(Instrumentation instrumentation) {
-            return instrumentationToStates.get(instrumentation);
+            return builder.await().thenApply(ChainedInstrumentationState::new);
         }
     }
 
@@ -529,5 +481,14 @@ public void onCompleted(Object result, Throwable t) {
             contexts.forEach(context -> context.onCompleted(result, t));
         }
     }
+
+    @FunctionalInterface
+    private interface ChainedInstrumentationFunction<I, S, A, R> {
+
+        R apply(I t, S u, A v);
+
+    }
+
+
 }
 
diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java
index 1041e9feb6..1928df84f0 100644
--- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java
+++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java
@@ -49,10 +49,7 @@ public NoContextChainedInstrumentation(Instrumentation... instrumentations) {
     }
 
     private <T> T runAll(InstrumentationState state, BiConsumer<Instrumentation, InstrumentationState> stateConsumer) {
-        for (Instrumentation instrumentation : instrumentations) {
-            InstrumentationState specificState = getSpecificState(instrumentation, state);
-            stateConsumer.accept(instrumentation, specificState);
-        }
+        chainedConsume(state, stateConsumer);
         return null;
     }
 


Test patch:
diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy
index 9328b5a6c6..135e1e5276 100644
--- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy
+++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy
@@ -21,9 +21,12 @@ class ChainedInstrumentationStateTest extends Specification {
         def a = new NamedInstrumentation("A")
         def b = new NamedInstrumentation("B")
         def c = new NamedInstrumentation("C")
+        def nullState = new SimplePerformantInstrumentation()
+
         def chainedInstrumentation = new ChainedInstrumentation([
                 a,
                 b,
+                nullState,
                 c,
         ])
 
diff --git a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java
new file mode 100644
index 0000000000..129d266765
--- /dev/null
+++ b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java
@@ -0,0 +1,77 @@
+package benchmark;
+
+import graphql.ExecutionInput;
+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.InstrumentationCreateStateParameters;
+import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
+import graphql.schema.GraphQLObjectType;
+import graphql.schema.GraphQLSchema;
+import org.jetbrains.annotations.NotNull;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static graphql.Scalars.GraphQLString;
+import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+import static graphql.schema.GraphQLObjectType.newObject;
+
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@Warmup(iterations = 2)
+@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS)
+public class ChainedInstrumentationBenchmark {
+
+    @Param({"0", "1", "10"})
+    public int num;
+
+    ChainedInstrumentation chainedInstrumentation;
+    GraphQLSchema schema;
+    InstrumentationExecutionParameters parameters;
+    InstrumentationState instrumentationState;
+
+    @Setup(Level.Trial)
+    public void setUp() throws ExecutionException, InterruptedException {
+        GraphQLObjectType queryType = newObject()
+                .name("benchmarkQuery")
+                .field(newFieldDefinition()
+                        .type(GraphQLString)
+                        .name("benchmark"))
+                .build();
+        schema = GraphQLSchema.newSchema()
+                .query(queryType)
+                .build();
+
+        ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("benchmark").build();
+        InstrumentationCreateStateParameters createStateParameters = new InstrumentationCreateStateParameters(schema, executionInput);
+
+        List<Instrumentation> instrumentations = Collections.nCopies(num, new SimplePerformantInstrumentation());
+        chainedInstrumentation = new ChainedInstrumentation(instrumentations);
+        instrumentationState = chainedInstrumentation.createStateAsync(createStateParameters).get();
+        parameters = new InstrumentationExecutionParameters(executionInput, schema, instrumentationState);
+    }
+
+    @Benchmark
+    public GraphQLSchema benchmarkInstrumentSchema() {
+        return chainedInstrumentation.instrumentSchema(schema, parameters, instrumentationState);
+    }
+
+    public static void main(String[] args) throws Exception {
+        Options opt = new OptionsBuilder()
+                .include("benchmark.ChainedInstrumentationBenchmark")
+                .forks(1)
+                .build();
+
+        new Runner(opt).run();
+    }
+
+}