From 9892efacfb46f910f488e02a3e9edf17825a73de Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 20 Oct 2026 14:28:38 -0400 Subject: [PATCH 1/2] add benchmark --- benchmark/defer-list-benchmark.js | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 benchmark/defer-list-benchmark.js diff --git a/benchmark/defer-list-benchmark.js b/benchmark/defer-list-benchmark.js new file mode 100644 index 0000000000..0f2bc506a3 --- /dev/null +++ b/benchmark/defer-list-benchmark.js @@ -0,0 +1,81 @@ +import { experimentalExecuteIncrementally } from 'graphql/execution/execute.js'; +import { parse } from 'graphql/language/parser.js'; +import { GraphQLList, GraphQLObjectType } from 'graphql/type/definition.js'; +import { + GraphQLDeferDirective, + specifiedDirectives, +} from 'graphql/type/directives.js'; +import { GraphQLString } from 'graphql/type/scalars.js'; +import { GraphQLSchema } from 'graphql/type/schema.js'; + +const User = new GraphQLObjectType({ + name: 'User', + fields: { + name: { type: GraphQLString }, + }, +}); + +const users = Array.from({ length: 600 }, (_, index) => ({ + name: `User ${index + 1}`, +})); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + allUsers: { + type: new GraphQLList(User), + resolve: () => users, + }, + }, + }), + directives: [...specifiedDirectives, GraphQLDeferDirective], + assumeValid: true, +}); + +const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); +const labels = []; +for (let i = 0; i < 31; i++) { + // 17.x.x works with `31` set to `26*26` + const letter1 = letters[Math.floor(i / 26)]; + const letter2 = letters[i % 26]; + labels.push(`${letter1}${letter2}`); +} + +const deferredSpreads = labels + .map((label) => ` ...A @defer(label: "${label}")`) + .join('\n'); + +let leafFields = 'name'; +for (let i = 0; i < 20; i++) { + leafFields += `\n ${letters[i]}:name`; +} + +const documentString = ` + { + allUsers { +${deferredSpreads} + } + } + fragment A on User { + ${leafFields} + } +`; +const document = parse(documentString); + +export const benchmark = { + name: 'Defer in Large List Benchmark', + measure: async () => { + const result = await experimentalExecuteIncrementally({ + schema, + document, + rootValue: {}, + }); + if ('initialResult' in result) { + // eslint-disable-next-line no-unused-vars + for await (const _subsequentResult of result.subsequentResults) { + // noop + } + } + }, +}; From f65ab8a969db415872e2d715fe5a00fe1c260d9a Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 20 Oct 2026 14:54:39 -0400 Subject: [PATCH 2/2] memoize pathToArray to reduce memory for multiple defers at the same path --- .../incremental/IncrementalPublisher.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/execution/incremental/IncrementalPublisher.ts b/src/execution/incremental/IncrementalPublisher.ts index 37bcdd083e..3a080ca638 100644 --- a/src/execution/incremental/IncrementalPublisher.ts +++ b/src/execution/incremental/IncrementalPublisher.ts @@ -1,4 +1,6 @@ +import { memoize1 } from '../../jsutils/memoize1.ts'; import type { ObjMap } from '../../jsutils/ObjMap.ts'; +import type { Path } from '../../jsutils/Path.ts'; import { pathToArray } from '../../jsutils/Path.ts'; import { ensureGraphQLError } from '../../error/ensureGraphQLError.ts'; @@ -36,9 +38,13 @@ export class IncrementalPublisher { private _ids: Map; private _nextId: number; + private _cachedPathToArray: (path: Path) => Array; constructor() { this._ids = new Map(); this._nextId = 0; + // Paths are shared by every defer label at one response position, so the + // flattened array is built once per Path instead of once per label. + this._cachedPathToArray = memoize1((path: Path) => pathToArray(path)); } buildResponse( @@ -88,6 +94,13 @@ export class IncrementalPublisher { }; } + private _pathToArray(path: Path | undefined): Array { + if (path == null) { + return []; + } + return this._cachedPathToArray(path); + } + private _ensureId( deferredFragmentOrStream: DeliveryGroup | ItemStream, ): string { @@ -110,7 +123,7 @@ export class IncrementalPublisher { const id = this._ensureId(node); const pendingResult: PendingResult = { id, - path: pathToArray(node.path), + path: this._pathToArray(node.path), }; if (node.label !== undefined) { pendingResult.label = node.label; @@ -267,7 +280,9 @@ export class IncrementalPublisher { initialDeferredFragmentRecord: DeliveryGroup, executionGroupValue: ExecutionGroupValue, ): { bestId: string; subPath: ReadonlyArray | undefined } { - let maxLength = pathToArray(initialDeferredFragmentRecord.path).length; + let maxLength = this._pathToArray( + initialDeferredFragmentRecord.path, + ).length; let bestId = initialId; for (const deliveryGroup of executionGroupValue.deliveryGroups) { @@ -279,7 +294,7 @@ export class IncrementalPublisher { if (id === undefined) { continue; } - const path = pathToArray(deliveryGroup.path); + const path = this._pathToArray(deliveryGroup.path); const length = path.length; if (length > maxLength) { maxLength = length;