Skip to content

Commit bf84048

Browse files
authored
fix: memoize collectSubfields and getStreamUsage across Executors (#4548)
and add tests!
1 parent 3b90551 commit bf84048

File tree

4 files changed

+165
-60
lines changed

4 files changed

+165
-60
lines changed

src/execution/Executor.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { invariant } from '../jsutils/invariant.js';
33
import { isAsyncIterable } from '../jsutils/isAsyncIterable.js';
44
import { isIterableObject } from '../jsutils/isIterableObject.js';
55
import { isPromise } from '../jsutils/isPromise.js';
6-
import { memoize1 } from '../jsutils/memoize1.js';
76
import { memoize2 } from '../jsutils/memoize2.js';
7+
import { memoize3 } from '../jsutils/memoize3.js';
88
import type { ObjMap } from '../jsutils/ObjMap.js';
99
import type { Path } from '../jsutils/Path.js';
1010
import { addPath, pathToArray } from '../jsutils/Path.js';
@@ -54,7 +54,7 @@ import {
5454
collectSubfields as _collectSubfields,
5555
} from './collectFields.js';
5656
import type { StreamUsage } from './getStreamUsage.js';
57-
import { getStreamUsage } from './getStreamUsage.js';
57+
import { getStreamUsage as _getStreamUsage } from './getStreamUsage.js';
5858
import type {
5959
DeferUsageSet,
6060
ExecutionPlan,
@@ -121,6 +121,37 @@ export interface ValidatedExecutionArgs {
121121
enableEarlyExecution: boolean;
122122
}
123123

124+
/**
125+
* A memoized collection of relevant subfields with regard to the return
126+
* type. Memoizing ensures the subfields are not repeatedly calculated, which
127+
* saves overhead when resolving lists of values.
128+
*/
129+
export const collectSubfields = memoize3(
130+
(
131+
validatedExecutionArgs: ValidatedExecutionArgs,
132+
returnType: GraphQLObjectType,
133+
fieldDetailsList: FieldDetailsList,
134+
) => {
135+
const { schema, fragments, variableValues, hideSuggestions } =
136+
validatedExecutionArgs;
137+
return _collectSubfields(
138+
schema,
139+
fragments,
140+
variableValues,
141+
returnType,
142+
fieldDetailsList,
143+
hideSuggestions,
144+
);
145+
},
146+
);
147+
148+
export const getStreamUsage = memoize2(
149+
(
150+
validatedExecutionArgs: ValidatedExecutionArgs,
151+
fieldDetailsList: FieldDetailsList,
152+
) => _getStreamUsage(validatedExecutionArgs, fieldDetailsList),
153+
);
154+
124155
/**
125156
* @internal
126157
*/
@@ -384,18 +415,6 @@ export class Executor {
384415
tasks: Array<ExecutionGroup>;
385416
streams: Array<ItemStream>;
386417

387-
collectSubfields: (
388-
returnType: GraphQLObjectType,
389-
fieldDetailsList: FieldDetailsList,
390-
) => {
391-
groupedFieldSet: GroupedFieldSet;
392-
newDeferUsages: ReadonlyArray<DeferUsage>;
393-
};
394-
395-
getStreamUsage: (
396-
fieldDetailsList: FieldDetailsList,
397-
) => StreamUsage | undefined;
398-
399418
constructor(
400419
validatedExecutionArgs: ValidatedExecutionArgs,
401420
deferUsageSet?: DeferUsageSet,
@@ -408,28 +427,6 @@ export class Executor {
408427
this.groups = [];
409428
this.tasks = [];
410429
this.streams = [];
411-
412-
/**
413-
* A memoized collection of relevant subfields with regard to the return
414-
* type. Memoizing ensures the subfields are not repeatedly calculated, which
415-
* saves overhead when resolving lists of values.
416-
*/
417-
this.collectSubfields = memoize2((returnType, fieldDetailsList) => {
418-
const { schema, fragments, variableValues, hideSuggestions } =
419-
this.validatedExecutionArgs;
420-
return _collectSubfields(
421-
schema,
422-
fragments,
423-
variableValues,
424-
returnType,
425-
fieldDetailsList,
426-
hideSuggestions,
427-
);
428-
});
429-
430-
this.getStreamUsage = memoize1((fieldDetailsList) =>
431-
getStreamUsage(this.validatedExecutionArgs, fieldDetailsList),
432-
);
433430
}
434431

435432
executeQueryOrMutationOrSubscriptionEvent(): PromiseOrValue<
@@ -1057,7 +1054,7 @@ export class Executor {
10571054
const streamUsage =
10581055
typeof path.key === 'number'
10591056
? undefined
1060-
: this.getStreamUsage(fieldDetailsList);
1057+
: getStreamUsage(this.validatedExecutionArgs, fieldDetailsList);
10611058

10621059
let containsPromise = false;
10631060
const completedResults: Array<unknown> = [];
@@ -1177,7 +1174,7 @@ export class Executor {
11771174
const streamUsage =
11781175
typeof path.key === 'number'
11791176
? undefined
1180-
: this.getStreamUsage(fieldDetailsList);
1177+
: getStreamUsage(this.validatedExecutionArgs, fieldDetailsList);
11811178

11821179
// This is specified as a simple map, however we're optimizing the path
11831180
// where the list contains no Promises by avoiding creating another Promise.
@@ -1549,7 +1546,8 @@ export class Executor {
15491546
deliveryGroupMap: ReadonlyMap<DeferUsage, DeliveryGroup> | undefined,
15501547
): PromiseOrValue<ObjMap<unknown>> {
15511548
// Collect sub-fields to execute to complete this value.
1552-
const { groupedFieldSet, newDeferUsages } = this.collectSubfields(
1549+
const { groupedFieldSet, newDeferUsages } = collectSubfields(
1550+
this.validatedExecutionArgs,
15531551
returnType,
15541552
fieldDetailsList,
15551553
);

src/execution/__tests__/executor-test.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
77
import { inspect } from '../../jsutils/inspect.js';
88
import { promiseWithResolvers } from '../../jsutils/promiseWithResolvers.js';
99

10+
import type { FieldNode } from '../../language/ast.js';
1011
import { Kind } from '../../language/kinds.js';
1112
import { parse } from '../../language/parser.js';
1213

@@ -31,7 +32,9 @@ import {
3132
} from '../../type/scalars.js';
3233
import { GraphQLSchema } from '../../type/schema.js';
3334

34-
import { execute, executeSync } from '../execute.js';
35+
import type { FieldDetailsList } from '../collectFields.js';
36+
import { execute, executeSync, validateExecutionArgs } from '../execute.js';
37+
import { collectSubfields, getStreamUsage } from '../Executor.js';
3538

3639
describe('Execute: Handles basic execution tasks', () => {
3740
it('executes arbitrary code', async () => {
@@ -1450,4 +1453,94 @@ describe('Execute: Handles basic execution tasks', () => {
14501453
],
14511454
});
14521455
});
1456+
1457+
it('memoizes collectSubfields results', () => {
1458+
const deepType = new GraphQLObjectType({
1459+
name: 'DeepType',
1460+
fields: {
1461+
name: { type: GraphQLString },
1462+
},
1463+
});
1464+
const schema = new GraphQLSchema({
1465+
query: new GraphQLObjectType({
1466+
name: 'Query',
1467+
fields: {
1468+
deep: { type: deepType },
1469+
},
1470+
}),
1471+
});
1472+
const document = parse('{ deep { name } }');
1473+
const validatedExecutionArgs = validateExecutionArgs({
1474+
schema,
1475+
document,
1476+
});
1477+
1478+
assert('schema' in validatedExecutionArgs);
1479+
1480+
const operation = validatedExecutionArgs.operation;
1481+
const node = operation.selectionSet.selections[0] as FieldNode;
1482+
1483+
const fieldDetailsList: FieldDetailsList = [{ node }];
1484+
1485+
const first = collectSubfields(
1486+
validatedExecutionArgs,
1487+
deepType,
1488+
fieldDetailsList,
1489+
);
1490+
1491+
const second = collectSubfields(
1492+
validatedExecutionArgs,
1493+
deepType,
1494+
fieldDetailsList,
1495+
);
1496+
1497+
expect(second).to.equal(first);
1498+
1499+
const third = collectSubfields(validatedExecutionArgs, deepType, [
1500+
{ node },
1501+
]);
1502+
1503+
expect(third).to.not.equal(first);
1504+
});
1505+
1506+
it('memoizes getStreamUsage results', () => {
1507+
const itemType = new GraphQLObjectType({
1508+
name: 'Item',
1509+
fields: {
1510+
id: { type: GraphQLString },
1511+
},
1512+
});
1513+
const schema = new GraphQLSchema({
1514+
query: new GraphQLObjectType({
1515+
name: 'Query',
1516+
fields: {
1517+
items: { type: new GraphQLList(itemType) },
1518+
},
1519+
}),
1520+
directives: [GraphQLStreamDirective],
1521+
});
1522+
const document = parse('{ items @stream(initialCount: 1) { id } }');
1523+
const validatedExecutionArgs = validateExecutionArgs({
1524+
schema,
1525+
document,
1526+
});
1527+
1528+
assert('schema' in validatedExecutionArgs);
1529+
1530+
const operation = validatedExecutionArgs.operation;
1531+
const node = operation.selectionSet.selections[0] as FieldNode;
1532+
1533+
const fieldDetailsList = [{ node }];
1534+
const first = getStreamUsage(validatedExecutionArgs, fieldDetailsList);
1535+
1536+
expect(first).to.not.equal(undefined);
1537+
1538+
const second = getStreamUsage(validatedExecutionArgs, fieldDetailsList);
1539+
1540+
expect(second).to.equal(first);
1541+
1542+
const third = getStreamUsage(validatedExecutionArgs, [{ node }]);
1543+
1544+
expect(third).to.not.equal(first);
1545+
});
14531546
});

src/jsutils/memoize1.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/jsutils/memoize3.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Memoizes the provided three-argument function.
3+
*/
4+
export function memoize3<
5+
A1 extends object,
6+
A2 extends object,
7+
A3 extends object,
8+
R,
9+
>(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R {
10+
let cache0: WeakMap<A1, WeakMap<A2, WeakMap<A3, R>>>;
11+
12+
return function memoized(a1, a2, a3) {
13+
cache0 ??= new WeakMap();
14+
15+
let cache1 = cache0.get(a1);
16+
if (cache1 === undefined) {
17+
cache1 = new WeakMap();
18+
cache0.set(a1, cache1);
19+
}
20+
21+
let cache2 = cache1.get(a2);
22+
if (cache2 === undefined) {
23+
cache2 = new WeakMap();
24+
cache1.set(a2, cache2);
25+
}
26+
27+
let fnResult = cache2.get(a3);
28+
if (fnResult === undefined) {
29+
fnResult = fn(a1, a2, a3);
30+
cache2.set(a3, fnResult);
31+
}
32+
return fnResult;
33+
};
34+
}

0 commit comments

Comments
 (0)