feat(execution): experimental batch field resolution#4760
Conversation
|
@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
FWIW, released today https://github.com/gmac/graphql-breadth-js |
|
From what I can tell graphql-breadth-js is pretty impressive! I'm surprised by my numbers above that grafast is not more competitive, I may be doing something wrong in terms of the measuring. There are hopefully a few things we could do to improve the batching implementation in export type GraphQLFieldExperimentalBatchResolver<
TSource,
TContext,
TArgs = any,
TResult = unknown,
> = (
sources: ReadonlyArray<TSource>,
args: TArgs,
context: TContext,
info: GraphQLBatchedResolveInfo,
) => PromiseOrValue<ReadonlyArray<TResult>>;
export interface GraphQLBatchedResolveInfo extends Omit<
GraphQLResolveInfo,
'path'
> {
/** Response paths for each source value in the batch. */
readonly paths: ReadonlyArray<Path>;
}Hopefully this PR gets the discussion going, looking for Golden Path fans input. @benjie |
|
Re: your surprise: Grafast is designed to help you reduce the amount of work that your application layer needs to do - fetching less data from your datasources, combining related work, removing redundant work, etc - and this benchmark doesn't have any such work (it's almost entirely synchronous, there isn't really an application layer to optimize) thus you're not going to see the benefits of Grafast execution, only the costs necessary to support them. Once you add network latency, bandwidth limits, and start leveraging things like attribute tracking to not even fetch attributes you don't need from your underlying datasources (which doesn't make sense when the datasources are just maps in memory as here) then the benefits of Grafast start to significantly outweigh the costs you see in synthetic benchmarks like this. For the synthetic benchmarks, simply eliminating the number of promises drastically reduces the amount of memory allocation and garbage collection needed, saving quite significant Node CPU time. You'd see even greater benefits if the base execution had all the resolvers wrapped with observability or similar - those costs quickly mount, so turning 10,000 resolver calls into a single batch resolver call does a lot to reduce the execution cost! |
5f14bc3 to
b8fbe1f
Compare
|
Looks like 1 and 10 should be tossed out of the memory lineup. The faster engines produced zero events. Also after seeing Benjie’s talk today, it really drove home the difference in problems we’re trying to solve. Grafast is trying to give you a standardized pattern for sound operational efficiencies. We build from the standpoint that the application is carefully structured for maximum efficiency through the resolver model, and we just want to squeeze as much out of scaling costs a possible. |
79d3127 to
10f2698
Compare
Introduce the experimental field batch resolver execution mode behind enableBatchResolvers: 'field'. The option is typed by BatchResolverLevel, and regular field execution remains the path for fields that do not define experimentalBatchResolve.
Example option:
execute({ schema, document, enableBatchResolvers: 'field' });
Example field resolver:
const name = {
type: GraphQLString,
experimentalBatchResolve: (sources) =>
sources.map((source) => source.name),
};
8d09aa1 to
d841932
Compare
#4627
Some numbers...
UPDATED: 2026-05-20UPDATED: 2026-05-21UPDATED: 2026-05-24UPDATED: 2026-05-26
Batch Resolution List-Field Benchmark
baseline=17.x.x modes=graphql-js-17,graphql-js-17-dataloader,graphql-js-local-batch,graphql-breadth-js,grafast,graphql-jit sizes=1,10,100,1000,10000 treeListDepths=1,5 treeListBreadths=10,100,1000 treeDepths=1,5,10,18 warmup=2000ms run=10000ms timingMinRounds=30 memoryIterations=500 memoryWarmup=100
Flat list
Speed
GC pressure
Tree within list
Speed
GC pressure
List with async widget field
Speed
GC pressure
Deep flat tree
Speed
GC pressure