Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/jmh/java/benchmark/VariableBatchBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package benchmark;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.VariableBatchExecutionResult;
import graphql.execution.preparsed.PreparsedDocumentEntry;
import graphql.schema.DataFetcher;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import org.dataloader.BatchLoader;
import org.dataloader.DataLoaderFactory;
import org.dataloader.DataLoaderRegistry;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/** Compares one variable batch with repeated ordinary executions sharing the same registry. */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class VariableBatchBenchmark {
private static final String QUERY = "query($id:Int!) { a:value(id:$id) b:value(id:$id) c:value(id:$id) }";

@Param({"16"})
public int size;

@Param({"false", "true"})
public boolean cachedDocument;

@Param({"false", "true"})
public boolean dataLoader;

private GraphQL graphQL;
private List<Map<String, Object>> variables;

@Setup
public void setup() {
DataFetcher<?> fetcher = environment -> dataLoader
? environment.getDataLoader("values").load(environment.getArgument("id"))
: environment.getArgument("id");
var schema = new SchemaGenerator().makeExecutableSchema(
new SchemaParser().parse("type Query { value(id:Int!):Int! }"),
RuntimeWiring.newRuntimeWiring().type("Query", type -> type.dataFetcher("value", fetcher)).build());
GraphQL.Builder builder = GraphQL.newGraphQL(schema);
if (cachedDocument) {
AtomicReference<PreparsedDocumentEntry> prepared = new AtomicReference<>();
builder.preparsedDocumentProvider((input, compute) -> CompletableFuture.completedFuture(
prepared.updateAndGet(value -> value == null ? compute.apply(input) : value)));
}
graphQL = builder.build();
variables = new ArrayList<>();
for (int index = 0; index < size; index++) {
variables.add(Map.of("id", index % 4));
}
}

@Benchmark
public List<ExecutionResult> variableBatch() {
ExecutionInput input = ExecutionInput.newExecutionInput(QUERY)
.variables(variables).dataLoaderRegistry(registry()).build();
VariableBatchExecutionResult result = (VariableBatchExecutionResult) graphQL.execute(input);
return result.getResultsAsync().join();
}

@Benchmark
public List<ExecutionResult> separateExecutions() {
DataLoaderRegistry registry = registry();
List<CompletableFuture<ExecutionResult>> results = new ArrayList<>();
for (Map<String, Object> values : variables) {
results.add(graphQL.executeAsync(ExecutionInput.newExecutionInput(QUERY)
.variables(values).dataLoaderRegistry(registry).build()));
}
CompletableFuture.allOf(results.toArray(new CompletableFuture<?>[0])).join();
List<ExecutionResult> completed = new ArrayList<>();
results.forEach(result -> completed.add(result.join()));
return completed;
}

private DataLoaderRegistry registry() {
DataLoaderRegistry registry = new DataLoaderRegistry();
if (dataLoader) {
BatchLoader<Object, Object> loader = keys -> CompletableFuture.completedFuture(keys);
registry.register("values", DataLoaderFactory.newDataLoader(loader));
}
return registry;
}
}
107 changes: 97 additions & 10 deletions src/main/java/graphql/ExecutionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import graphql.execution.ExecutionId;
import graphql.execution.RawVariables;
import graphql.execution.preparsed.persisted.PersistedQuerySupport;
import graphql.extensions.ExtensionsBuilder;
import org.dataloader.DataLoaderRegistry;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand All @@ -24,13 +26,16 @@
@PublicApi
@NullMarked
public class ExecutionInput {
private static final List<RawVariables> EMPTY_VARIABLES_LIST = List.of(RawVariables.emptyVariables());
private final String query;
private final String operationName;
private final @Nullable Object context;
private final GraphQLContext graphQLContext;
private final @Nullable Object localContext;
private final Object root;
private final RawVariables rawVariables;
private final List<RawVariables> rawVariablesList;
private final @Nullable Integer variableIndex;
private final @Nullable ExecutionId variableBatchId;
private final Map<String, Object> extensions;
private final DataLoaderRegistry dataLoaderRegistry;
private final ExecutionId executionId;
Expand All @@ -55,7 +60,9 @@ private ExecutionInput(Builder builder) {
this.context = builder.context;
this.graphQLContext = assertNotNull(builder.graphQLContext);
this.root = builder.root;
this.rawVariables = builder.rawVariables;
this.rawVariablesList = builder.rawVariablesList;
this.variableIndex = builder.variableIndex;
this.variableBatchId = builder.variableBatchId;
this.dataLoaderRegistry = builder.dataLoaderRegistry;
this.executionId = builder.executionId;
this.locale = builder.locale != null ? builder.locale : Locale.getDefault(); // always have a locale in place
Expand Down Expand Up @@ -143,16 +150,74 @@ public Object getRoot() {

/**
* @return a map of raw variables that can be referenced via $syntax in the query.
* @throws AssertException if more than one variable map is present; use {@link #getVariablesList()} instead
*/
public Map<String, Object> getVariables() {
return rawVariables.toMap();
return getRawVariables().toMap();
}

/**
* @return a map of raw variables that can be referenced via $syntax in the query.
* @throws AssertException if more than one variable map is present; use {@link #getVariablesList()} instead
*/
public RawVariables getRawVariables() {
return rawVariables;
Assert.assertTrue(rawVariablesList.size() <= 1,
"getRawVariables() requires at most one variable map; use getVariablesList() for variable batches");
return rawVariablesList.isEmpty() ? RawVariables.emptyVariables() : rawVariablesList.get(0);
}

/**
* Returns the variable maps in input order. An empty list or a singleton uses ordinary execution;
* a larger list executes the selected operation once per map, sharing the DataLoader registry.
* The {@link #getVariables()} and {@link #getRawVariables()} accessors require at most one map,
* and return an empty map when the list is empty.
*
* @return the immutable list of immutable variable maps (values can be null)
*/
@ExperimentalApi
public List<Map<String, Object>> getVariablesList() {
return ImmutableKit.map(rawVariablesList, RawVariables::toMap);
}

/** @return whether this input requests multiple executions */
@ExperimentalApi
public boolean isVariableBatch() {
return rawVariablesList.size() > 1;
}

/** @return the zero-based index for an individual batch execution, otherwise null */
@ExperimentalApi
public @Nullable Integer getVariableIndex() {
return variableIndex;
}

/** @return the shared batch id for an individual batch execution, otherwise null */
@ExperimentalApi
public @Nullable ExecutionId getVariableBatchId() {
return variableBatchId;
}

@Internal
public ExecutionInput newVariableBatchEntry(int index, ExecutionId batchId) {
GraphQLContext entryContext = GraphQLContext.newContext().build().putAll(graphQLContext);
ExtensionsBuilder extensionsBuilder = graphQLContext.get(ExtensionsBuilder.class);
if (extensionsBuilder != null) {
entryContext.put(ExtensionsBuilder.class, extensionsBuilder.copy());
}
Builder builder = newExecutionInput(query)
.operationName(operationName)
.internalTransferContext(entryContext)
.context(context == graphQLContext ? entryContext : context)
.localContext(localContext)
.root(root)
.variables(rawVariablesList.get(index).toMap())
.extensions(extensions)
.dataLoaderRegistry(dataLoaderRegistry)
.locale(locale)
.profileExecution(profileExecution);
builder.variableIndex = index;
builder.variableBatchId = batchId;
return builder.build();
}

/**
Expand Down Expand Up @@ -257,10 +322,12 @@ public ExecutionInput transform(Consumer<Builder> builderConsumer) {
.localContext(this.localContext)
.root(this.root)
.dataLoaderRegistry(this.dataLoaderRegistry)
.variables(this.rawVariables.toMap())
.extensions(this.extensions)
.executionId(this.executionId)
.locale(this.locale);
builder.rawVariablesList = this.rawVariablesList;
builder.variableIndex = this.variableIndex;
builder.variableBatchId = this.variableBatchId;

builderConsumer.accept(builder);

Expand All @@ -275,7 +342,7 @@ public String toString() {
", context=" + context +
", graphQLContext=" + graphQLContext +
", root=" + root +
", rawVariables=" + rawVariables +
", rawVariablesList=" + rawVariablesList +
", dataLoaderRegistry=" + dataLoaderRegistry +
", executionId= " + executionId +
", locale= " + locale +
Expand Down Expand Up @@ -309,7 +376,9 @@ public static class Builder {
private Object context = graphQLContext; // we make these the same object on purpose - legacy code will get the same object if this change nothing
private Object localContext;
private Object root;
private RawVariables rawVariables = RawVariables.emptyVariables();
private List<RawVariables> rawVariablesList = EMPTY_VARIABLES_LIST;
private Integer variableIndex;
private ExecutionId variableBatchId;
private Map<String, Object> extensions = ImmutableKit.emptyMap();
//
// this is important - it allows code to later known if we never really set a dataloader and hence it can optimize
Expand Down Expand Up @@ -444,7 +513,23 @@ public Builder root(Object root) {
*/
public Builder variables(Map<String, Object> rawVariables) {
assertNotNull(rawVariables, "variables map can't be null");
this.rawVariables = RawVariables.of(rawVariables);
this.rawVariablesList = List.of(RawVariables.of(rawVariables));
return this;
}

/**
* Supplies complete variable maps for the same selected operation. An empty list executes once
* with empty variables; a singleton executes normally. Larger lists return a
* {@link VariableBatchExecutionResult}. Entries are never deduplicated. Subscriptions cannot be batched.
*
* @param variables the maps in input order; neither the list nor its entries may be null
* @return this builder
*/
@ExperimentalApi
public Builder variables(List<Map<String, Object>> variables) {
assertNotNull(variables, "variables list can't be null");
this.rawVariablesList = List.copyOf(ImmutableKit.map(variables,
value -> RawVariables.of(assertNotNull(value, "variables list entry can't be null"))));
return this;
}

Expand All @@ -456,7 +541,9 @@ public Builder extensions(Map<String, Object> extensions) {
/**
* You should create new {@link org.dataloader.DataLoaderRegistry}s and new {@link org.dataloader.DataLoader}s for each execution. Do not
* re-use
* instances as this will create unexpected results.
* instances across independent requests as this will create unexpected results. Variable batching
* shares the supplied registry and its caches across all entries in that batch, including mutations.
* Mutation resolvers should clear or prime affected cache entries as usual.
*
* @param dataLoaderRegistry a registry of {@link org.dataloader.DataLoader}s
*
Expand All @@ -476,4 +563,4 @@ public ExecutionInput build() {
return new ExecutionInput(this);
}
}
}
}
Loading
Loading