From 11059997c4979316597b91dd8f0646bacf0ea382 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 8 Sep 2024 21:07:16 +0300 Subject: [PATCH 1/5] polish(fragmentArgs): simplify fragment args execution by creating a map for each fragment with fragment variables with the properly combined local and global variables background: this was in the original PR #4015, was taken out by me when i misunderstood and thought that when a fragment variable shadows an operation variable, the operation variable could still leak through if the fragment variable had no default, and I thought it was necessary within getArgumentValues to still have the signatures advantages to the original approach, which this PR restores: does not change the signature of getVariableValues, getDirectiveValues, valueFromAST disadvantages: creating a combined variable map of local and global variables may be a waste if the global variables are not actually used in that fragment --- src/execution/collectFields.ts | 79 ++++++++++++++-------------------- src/execution/execute.ts | 8 ++-- src/execution/values.ts | 20 ++------- src/utilities/valueFromAST.ts | 37 +++------------- 4 files changed, 46 insertions(+), 98 deletions(-) diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 9146312f70..dee17e7353 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -32,15 +32,10 @@ export interface DeferUsage { parentDeferUsage: DeferUsage | undefined; } -export interface FragmentVariables { - signatures: ObjMap; - values: ObjMap; -} - export interface FieldDetails { node: FieldNode; deferUsage?: DeferUsage | undefined; - fragmentVariables?: FragmentVariables | undefined; + fragmentVariableValues?: { [variable: string]: unknown } | undefined; } export type FieldGroup = ReadonlyArray; @@ -136,14 +131,14 @@ export function collectSubfields( for (const fieldDetail of fieldGroup) { const selectionSet = fieldDetail.node.selectionSet; if (selectionSet) { - const { deferUsage, fragmentVariables } = fieldDetail; + const { deferUsage, fragmentVariableValues } = fieldDetail; collectFieldsImpl( context, selectionSet, subGroupedFieldSet, newDeferUsages, deferUsage, - fragmentVariables, + fragmentVariableValues, ); } } @@ -161,7 +156,7 @@ function collectFieldsImpl( groupedFieldSet: AccumulatorMap, newDeferUsages: Array, deferUsage?: DeferUsage, - fragmentVariables?: FragmentVariables, + fragmentVariableValues?: { [variable: string]: unknown }, ): void { const { schema, @@ -172,22 +167,24 @@ function collectFieldsImpl( visitedFragmentNames, } = context; + const scopedVariableValues = fragmentVariableValues ?? variableValues; + for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: { - if (!shouldIncludeNode(selection, variableValues, fragmentVariables)) { + if (!shouldIncludeNode(scopedVariableValues, selection)) { continue; } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, deferUsage, - fragmentVariables, + fragmentVariableValues, }); break; } case Kind.INLINE_FRAGMENT: { if ( - !shouldIncludeNode(selection, variableValues, fragmentVariables) || + !shouldIncludeNode(scopedVariableValues, selection) || !doesFragmentConditionMatch(schema, selection, runtimeType) ) { continue; @@ -195,8 +192,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, - variableValues, - fragmentVariables, + scopedVariableValues, selection, deferUsage, ); @@ -208,7 +204,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - fragmentVariables, + fragmentVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -218,7 +214,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - fragmentVariables, + fragmentVariableValues, ); } @@ -229,8 +225,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, - variableValues, - fragmentVariables, + scopedVariableValues, selection, deferUsage, ); @@ -238,7 +233,7 @@ function collectFieldsImpl( if ( !newDeferUsage && (visitedFragmentNames.has(fragName) || - !shouldIncludeNode(selection, variableValues, fragmentVariables)) + !shouldIncludeNode(scopedVariableValues, selection)) ) { continue; } @@ -252,17 +247,20 @@ function collectFieldsImpl( } const fragmentVariableSignatures = fragment.variableSignatures; - let newFragmentVariables: FragmentVariables | undefined; + let newFragmentVariableValues: + | { [variable: string]: unknown } + | undefined; if (fragmentVariableSignatures) { - newFragmentVariables = { - signatures: fragmentVariableSignatures, - values: experimentalGetArgumentValues( - selection, - Object.values(fragmentVariableSignatures), - variableValues, - fragmentVariables, - ), - }; + newFragmentVariableValues = experimentalGetArgumentValues( + selection, + Object.values(fragmentVariableSignatures), + scopedVariableValues, + ); + for (const [variableName, value] of Object.entries(variableValues)) { + if (!fragment.variableSignatures?.[variableName]) { + newFragmentVariableValues[variableName] = value; + } + } } if (!newDeferUsage) { @@ -273,7 +271,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - newFragmentVariables, + newFragmentVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -283,7 +281,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - newFragmentVariables, + newFragmentVariableValues, ); } break; @@ -300,16 +298,10 @@ function collectFieldsImpl( function getDeferUsage( operation: OperationDefinitionNode, variableValues: { [variable: string]: unknown }, - fragmentVariables: FragmentVariables | undefined, node: FragmentSpreadNode | InlineFragmentNode, parentDeferUsage: DeferUsage | undefined, ): DeferUsage | undefined { - const defer = getDirectiveValues( - GraphQLDeferDirective, - node, - variableValues, - fragmentVariables, - ); + const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues); if (!defer) { return; @@ -335,16 +327,10 @@ function getDeferUsage( * directives, where `@skip` has higher precedence than `@include`. */ function shouldIncludeNode( - node: FragmentSpreadNode | FieldNode | InlineFragmentNode, variableValues: { [variable: string]: unknown }, - fragmentVariables: FragmentVariables | undefined, + node: FragmentSpreadNode | FieldNode | InlineFragmentNode, ): boolean { - const skip = getDirectiveValues( - GraphQLSkipDirective, - node, - variableValues, - fragmentVariables, - ); + const skip = getDirectiveValues(GraphQLSkipDirective, node, variableValues); if (skip?.if === true) { return false; } @@ -353,7 +339,6 @@ function shouldIncludeNode( GraphQLIncludeDirective, node, variableValues, - fragmentVariables, ); if (include?.if === false) { return false; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 97e88f07ad..4c3dd65cfd 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -752,8 +752,7 @@ function executeField( const args = experimentalGetArgumentValues( fieldGroup[0].node, fieldDef.args, - exeContext.variableValues, - fieldGroup[0].fragmentVariables, + fieldGroup[0].fragmentVariableValues ?? exeContext.variableValues, ); // The resolve function's optional third argument is a context value that @@ -1061,8 +1060,7 @@ function getStreamUsage( const stream = getDirectiveValues( GraphQLStreamDirective, fieldGroup[0].node, - exeContext.variableValues, - fieldGroup[0].fragmentVariables, + fieldGroup[0].fragmentVariableValues ?? exeContext.variableValues, ); if (!stream) { @@ -1091,7 +1089,7 @@ function getStreamUsage( const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ node: fieldDetails.node, deferUsage: undefined, - fragmentVariables: fieldDetails.fragmentVariables, + fragmentVariableValues: fieldDetails.fragmentVariableValues, })); const streamUsage = { diff --git a/src/execution/values.ts b/src/execution/values.ts index 23863fd107..5896f85ba2 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -22,7 +22,6 @@ import type { GraphQLSchema } from '../type/schema.js'; import { coerceInputValue } from '../utilities/coerceInputValue.js'; import { valueFromAST } from '../utilities/valueFromAST.js'; -import type { FragmentVariables } from './collectFields.js'; import type { GraphQLVariableSignature } from './getVariableSignature.js'; import { getVariableSignature } from './getVariableSignature.js'; @@ -156,7 +155,6 @@ export function experimentalGetArgumentValues( node: FieldNode | DirectiveNode | FragmentSpreadNode, argDefs: ReadonlyArray, variableValues: Maybe>, - fragmentVariables?: Maybe, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -188,12 +186,9 @@ export function experimentalGetArgumentValues( if (valueNode.kind === Kind.VARIABLE) { const variableName = valueNode.name.value; - const scopedVariableValues = fragmentVariables?.signatures[variableName] - ? fragmentVariables.values - : variableValues; if ( - scopedVariableValues == null || - !Object.hasOwn(scopedVariableValues, variableName) + variableValues == null || + !Object.hasOwn(variableValues, variableName) ) { if (argDef.defaultValue !== undefined) { coercedValues[name] = argDef.defaultValue; @@ -206,7 +201,7 @@ export function experimentalGetArgumentValues( } continue; } - isNull = scopedVariableValues[variableName] == null; + isNull = variableValues[variableName] == null; } if (isNull && isNonNullType(argType)) { @@ -217,12 +212,7 @@ export function experimentalGetArgumentValues( ); } - const coercedValue = valueFromAST( - valueNode, - argType, - variableValues, - fragmentVariables?.values, - ); + const coercedValue = valueFromAST(valueNode, argType, variableValues); if (coercedValue === undefined) { // Note: ValuesOfCorrectTypeRule validation should catch this before // execution. This is a runtime check to ensure execution does not @@ -254,7 +244,6 @@ export function getDirectiveValues( directiveDef: GraphQLDirective, node: { readonly directives?: ReadonlyArray | undefined }, variableValues?: Maybe>, - fragmentVariables?: Maybe, ): undefined | { [argument: string]: unknown } { const directiveNode = node.directives?.find( (directive) => directive.name.value === directiveDef.name, @@ -265,7 +254,6 @@ export function getDirectiveValues( directiveNode, directiveDef.args, variableValues, - fragmentVariables, ); } } diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index 3aec3f272f..9dee860995 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -38,7 +38,6 @@ export function valueFromAST( valueNode: Maybe, type: GraphQLInputType, variables?: Maybe>, - fragmentVariables?: Maybe>, ): unknown { if (!valueNode) { // When there is no node, then there is also no value. @@ -48,8 +47,7 @@ export function valueFromAST( if (valueNode.kind === Kind.VARIABLE) { const variableName = valueNode.name.value; - const variableValue = - fragmentVariables?.[variableName] ?? variables?.[variableName]; + const variableValue = variables?.[variableName]; if (variableValue === undefined) { // No valid return value. return; @@ -67,7 +65,7 @@ export function valueFromAST( if (valueNode.kind === Kind.NULL) { return; // Invalid: intentionally return no value. } - return valueFromAST(valueNode, type.ofType, variables, fragmentVariables); + return valueFromAST(valueNode, type.ofType, variables); } if (valueNode.kind === Kind.NULL) { @@ -80,7 +78,7 @@ export function valueFromAST( if (valueNode.kind === Kind.LIST) { const coercedValues = []; for (const itemNode of valueNode.values) { - if (isMissingVariable(itemNode, variables, fragmentVariables)) { + if (isMissingVariable(itemNode, variables)) { // If an array contains a missing variable, it is either coerced to // null or if the item type is non-null, it considered invalid. if (isNonNullType(itemType)) { @@ -88,12 +86,7 @@ export function valueFromAST( } coercedValues.push(null); } else { - const itemValue = valueFromAST( - itemNode, - itemType, - variables, - fragmentVariables, - ); + const itemValue = valueFromAST(itemNode, itemType, variables); if (itemValue === undefined) { return; // Invalid: intentionally return no value. } @@ -102,12 +95,7 @@ export function valueFromAST( } return coercedValues; } - const coercedValue = valueFromAST( - valueNode, - itemType, - variables, - fragmentVariables, - ); + const coercedValue = valueFromAST(valueNode, itemType, variables); if (coercedValue === undefined) { return; // Invalid: intentionally return no value. } @@ -124,10 +112,7 @@ export function valueFromAST( ); for (const field of Object.values(type.getFields())) { const fieldNode = fieldNodes.get(field.name); - if ( - fieldNode == null || - isMissingVariable(fieldNode.value, variables, fragmentVariables) - ) { + if (fieldNode == null || isMissingVariable(fieldNode.value, variables)) { if (field.defaultValue !== undefined) { coercedObj[field.name] = field.defaultValue; } else if (isNonNullType(field.type)) { @@ -135,12 +120,7 @@ export function valueFromAST( } continue; } - const fieldValue = valueFromAST( - fieldNode.value, - field.type, - variables, - fragmentVariables, - ); + const fieldValue = valueFromAST(fieldNode.value, field.type, variables); if (fieldValue === undefined) { return; // Invalid: intentionally return no value. } @@ -186,12 +166,9 @@ export function valueFromAST( function isMissingVariable( valueNode: ValueNode, variables: Maybe>, - fragmentVariables: Maybe>, ): boolean { return ( valueNode.kind === Kind.VARIABLE && - (fragmentVariables == null || - fragmentVariables[valueNode.name.value] === undefined) && (variables == null || variables[valueNode.name.value] === undefined) ); } From 884f67cd74a345d23ca99ef5863f5ca69c29cb71 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 9 Sep 2024 13:39:27 +0300 Subject: [PATCH 2/5] rename? --- src/execution/collectFields.ts | 44 ++++++++++++++++++---------------- src/execution/execute.ts | 6 ++--- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index dee17e7353..66b0eb5d43 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -35,7 +35,7 @@ export interface DeferUsage { export interface FieldDetails { node: FieldNode; deferUsage?: DeferUsage | undefined; - fragmentVariableValues?: { [variable: string]: unknown } | undefined; + scopedVariableValues?: { [variable: string]: unknown } | undefined; } export type FieldGroup = ReadonlyArray; @@ -131,14 +131,14 @@ export function collectSubfields( for (const fieldDetail of fieldGroup) { const selectionSet = fieldDetail.node.selectionSet; if (selectionSet) { - const { deferUsage, fragmentVariableValues } = fieldDetail; + const { deferUsage, scopedVariableValues } = fieldDetail; collectFieldsImpl( context, selectionSet, subGroupedFieldSet, newDeferUsages, deferUsage, - fragmentVariableValues, + scopedVariableValues, ); } } @@ -156,35 +156,35 @@ function collectFieldsImpl( groupedFieldSet: AccumulatorMap, newDeferUsages: Array, deferUsage?: DeferUsage, - fragmentVariableValues?: { [variable: string]: unknown }, + scopedVariableValues?: { [variable: string]: unknown }, ): void { const { schema, fragments, - variableValues, + variableValues: operationVariableValues, runtimeType, operation, visitedFragmentNames, } = context; - const scopedVariableValues = fragmentVariableValues ?? variableValues; + const variableValues = scopedVariableValues ?? operationVariableValues; for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: { - if (!shouldIncludeNode(scopedVariableValues, selection)) { + if (!shouldIncludeNode(variableValues, selection)) { continue; } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, deferUsage, - fragmentVariableValues, + scopedVariableValues, }); break; } case Kind.INLINE_FRAGMENT: { if ( - !shouldIncludeNode(scopedVariableValues, selection) || + !shouldIncludeNode(variableValues, selection) || !doesFragmentConditionMatch(schema, selection, runtimeType) ) { continue; @@ -192,7 +192,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, - scopedVariableValues, + variableValues, selection, deferUsage, ); @@ -204,7 +204,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - fragmentVariableValues, + scopedVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -214,7 +214,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - fragmentVariableValues, + scopedVariableValues, ); } @@ -225,7 +225,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, - scopedVariableValues, + variableValues, selection, deferUsage, ); @@ -233,7 +233,7 @@ function collectFieldsImpl( if ( !newDeferUsage && (visitedFragmentNames.has(fragName) || - !shouldIncludeNode(scopedVariableValues, selection)) + !shouldIncludeNode(variableValues, selection)) ) { continue; } @@ -247,18 +247,20 @@ function collectFieldsImpl( } const fragmentVariableSignatures = fragment.variableSignatures; - let newFragmentVariableValues: + let newScopedVariableValues: | { [variable: string]: unknown } | undefined; if (fragmentVariableSignatures) { - newFragmentVariableValues = experimentalGetArgumentValues( + newScopedVariableValues = experimentalGetArgumentValues( selection, Object.values(fragmentVariableSignatures), - scopedVariableValues, + variableValues, ); - for (const [variableName, value] of Object.entries(variableValues)) { + for (const [variableName, value] of Object.entries( + operationVariableValues, + )) { if (!fragment.variableSignatures?.[variableName]) { - newFragmentVariableValues[variableName] = value; + newScopedVariableValues[variableName] = value; } } } @@ -271,7 +273,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - newFragmentVariableValues, + newScopedVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -281,7 +283,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - newFragmentVariableValues, + newScopedVariableValues, ); } break; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4c3dd65cfd..7a36792eec 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -752,7 +752,7 @@ function executeField( const args = experimentalGetArgumentValues( fieldGroup[0].node, fieldDef.args, - fieldGroup[0].fragmentVariableValues ?? exeContext.variableValues, + fieldGroup[0].scopedVariableValues ?? exeContext.variableValues, ); // The resolve function's optional third argument is a context value that @@ -1060,7 +1060,7 @@ function getStreamUsage( const stream = getDirectiveValues( GraphQLStreamDirective, fieldGroup[0].node, - fieldGroup[0].fragmentVariableValues ?? exeContext.variableValues, + fieldGroup[0].scopedVariableValues ?? exeContext.variableValues, ); if (!stream) { @@ -1089,7 +1089,7 @@ function getStreamUsage( const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ node: fieldDetails.node, deferUsage: undefined, - fragmentVariableValues: fieldDetails.fragmentVariableValues, + scopedVariableValues: fieldDetails.scopedVariableValues, })); const streamUsage = { From 98f4c7755724f1451371f298ed280fbd70e5acf7 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 9 Sep 2024 22:04:51 +0300 Subject: [PATCH 3/5] extract out to function --- src/execution/collectFields.ts | 48 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 66b0eb5d43..0bd2b7b76f 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -246,24 +246,12 @@ function collectFieldsImpl( continue; } - const fragmentVariableSignatures = fragment.variableSignatures; - let newScopedVariableValues: - | { [variable: string]: unknown } - | undefined; - if (fragmentVariableSignatures) { - newScopedVariableValues = experimentalGetArgumentValues( - selection, - Object.values(fragmentVariableSignatures), - variableValues, - ); - for (const [variableName, value] of Object.entries( - operationVariableValues, - )) { - if (!fragment.variableSignatures?.[variableName]) { - newScopedVariableValues[variableName] = value; - } - } - } + const newScopedVariableValues = getScopedVariableValues( + fragment.variableSignatures, + selection, + variableValues, + operationVariableValues, + ); if (!newDeferUsage) { visitedFragmentNames.add(fragName); @@ -370,6 +358,30 @@ function doesFragmentConditionMatch( return false; } +/** + * Constructs a variableValues map for the given fragment. + */ +function getScopedVariableValues( + fragmentVariableSignatures: ObjMap | undefined, + fragmentSpreadNode: FragmentSpreadNode, + variableValues: { [variable: string]: unknown }, + operationVariableValues: { [variable: string]: unknown }, +): { [variable: string]: unknown } | undefined { + if (!fragmentVariableSignatures) { + return; + } + const scopedVariableValues = experimentalGetArgumentValues( + fragmentSpreadNode, + Object.values(fragmentVariableSignatures), + variableValues, + ); + for (const [variableName, value] of Object.entries(operationVariableValues)) { + if (fragmentVariableSignatures[variableName] === undefined) { + scopedVariableValues[variableName] = value; + } + } +} + /** * Implements the logic to compute the key of a given field's entry */ From b0add3b7202cfa12243ef7ed061b27fc209734b1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 9 Sep 2024 22:10:27 +0300 Subject: [PATCH 4/5] f --- src/execution/collectFields.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 0bd2b7b76f..1850e819fb 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -380,6 +380,7 @@ function getScopedVariableValues( scopedVariableValues[variableName] = value; } } + return scopedVariableValues; } /** From c523396eacc305c957b878e5497d2d8194a361eb Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 9 Sep 2024 23:05:17 +0300 Subject: [PATCH 5/5] add benchmark --- benchmark/fragment-arguments-benchmark.js | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 benchmark/fragment-arguments-benchmark.js diff --git a/benchmark/fragment-arguments-benchmark.js b/benchmark/fragment-arguments-benchmark.js new file mode 100644 index 0000000000..b718847256 --- /dev/null +++ b/benchmark/fragment-arguments-benchmark.js @@ -0,0 +1,48 @@ +import { execute } from 'graphql/execution/execute.js'; +import { parse } from 'graphql/language/parser.js'; +import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; + +const schema = buildSchema('type Query { field(echo: String!): [String] }'); + +const integers = Array.from({ length: 100 }, (_, i) => i + 1); + +const document = parse( + ` + query WithManyFragmentArguments(${integers + .map((i) => `$echo${i}: String`) + .join(', ')}) { + ${integers.map((i) => `echo${i}: field(echo: $echo${i})`).join('\n')} + ${integers.map((i) => `...EchoFragment${i}(echo: $echo${i})`).join('\n')} + } + + ${integers + .map( + (i) => + `fragment EchoFragment${i}($echo: String) on Query { echoFragment${i}: field(echo: $echo) }`, + ) + .join('\n')} + `, + { experimentalFragmentArguments: true }, +); + +const variableValues = Object.create(null); +for (const i of integers) { + variableValues[`echo${i}`] = `Echo ${i}`; +} + +function field(_, args) { + return args.echo; +} + +export const benchmark = { + name: 'Execute Operation with Fragment Arguments', + count: 10, + async measure() { + await execute({ + schema, + document, + rootValue: { field }, + variableValues, + }); + }, +};