Skip to content

Commit 78e9df7

Browse files
committed
polish(incremental): lazily create DeferredFragments
goal: avoid creating or passing around the deferMap methodology: each DeferredFragmentRecord will be unique for a given deferUsage and creationPath - we annotate the deferUsage with a "depth" property representing the path length in the response for wherever this defer is delivered. - from a given execution group path, we can derive the path for the deferredFragment for a given deferUsage by "rewinding" the execution group path to the depth annotated on the given deferUsage
1 parent bdb0553 commit 78e9df7

File tree

8 files changed

+303
-296
lines changed

8 files changed

+303
-296
lines changed

Diff for: src/execution/DeferredFragments.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { Path } from '../jsutils/Path.js';
2+
3+
import type { DeferUsage } from './collectFields.js';
4+
import type {
5+
PendingExecutionGroup,
6+
StreamRecord,
7+
SuccessfulExecutionGroup,
8+
} from './types.js';
9+
10+
export type DeliveryGroup = DeferredFragmentRecord | StreamRecord;
11+
12+
/** @internal */
13+
export class DeferredFragmentRecord {
14+
path: Path | undefined;
15+
label: string | undefined;
16+
parentDeferUsage: DeferUsage | undefined;
17+
id?: string | undefined;
18+
pendingExecutionGroups: Set<PendingExecutionGroup>;
19+
successfulExecutionGroups: Set<SuccessfulExecutionGroup>;
20+
children: Set<DeliveryGroup>;
21+
22+
constructor(
23+
path: Path | undefined,
24+
label: string | undefined,
25+
parentDeferUsage: DeferUsage | undefined,
26+
) {
27+
this.path = path;
28+
this.label = label;
29+
this.parentDeferUsage = parentDeferUsage;
30+
this.pendingExecutionGroups = new Set();
31+
this.successfulExecutionGroups = new Set();
32+
this.children = new Set();
33+
}
34+
}
35+
36+
export function isDeferredFragmentRecord(
37+
deliveryGroup: DeliveryGroup,
38+
): deliveryGroup is DeferredFragmentRecord {
39+
return deliveryGroup instanceof DeferredFragmentRecord;
40+
}
41+
42+
/**
43+
* @internal
44+
*/
45+
export class DeferredFragmentFactory {
46+
private _rootDeferredFragments = new Map<
47+
DeferUsage,
48+
DeferredFragmentRecord
49+
>();
50+
51+
get(deferUsage: DeferUsage, path: Path | undefined): DeferredFragmentRecord {
52+
const deferUsagePath = this._pathAtDepth(path, deferUsage.depth);
53+
let deferredFragmentRecords:
54+
| Map<DeferUsage, DeferredFragmentRecord>
55+
| undefined;
56+
if (deferUsagePath === undefined) {
57+
deferredFragmentRecords = this._rootDeferredFragments;
58+
} else {
59+
// A doubly nested Map<Path, Map<DeferUsage, DeferredFragmentRecord>>
60+
// could be used, but could leak memory in long running operations.
61+
// A WeakMap could be used instead. The below implementation is
62+
// WeakMap-Like, saving the Map on the Path object directly.
63+
// Alternatively, memory could be reclaimed manually, taking care to
64+
// also reclaim memory for nested DeferredFragmentRecords if the parent
65+
// is removed secondary to an error.
66+
deferredFragmentRecords = (
67+
deferUsagePath as unknown as {
68+
deferredFragmentRecords: Map<DeferUsage, DeferredFragmentRecord>;
69+
}
70+
).deferredFragmentRecords;
71+
if (deferredFragmentRecords === undefined) {
72+
deferredFragmentRecords = new Map();
73+
(
74+
deferUsagePath as unknown as {
75+
deferredFragmentRecords: Map<DeferUsage, DeferredFragmentRecord>;
76+
}
77+
).deferredFragmentRecords = deferredFragmentRecords;
78+
}
79+
}
80+
let deferredFragmentRecord = deferredFragmentRecords.get(deferUsage);
81+
if (deferredFragmentRecord === undefined) {
82+
const { label, parentDeferUsage } = deferUsage;
83+
deferredFragmentRecord = new DeferredFragmentRecord(
84+
deferUsagePath,
85+
label,
86+
parentDeferUsage,
87+
);
88+
deferredFragmentRecords.set(deferUsage, deferredFragmentRecord);
89+
}
90+
return deferredFragmentRecord;
91+
}
92+
93+
private _pathAtDepth(
94+
path: Path | undefined,
95+
depth: number,
96+
): Path | undefined {
97+
if (depth === 0) {
98+
return;
99+
}
100+
const stack: Array<Path> = [];
101+
let currentPath = path;
102+
while (currentPath !== undefined) {
103+
stack.unshift(currentPath);
104+
currentPath = currentPath.prev;
105+
}
106+
return stack[depth - 1];
107+
}
108+
}

Diff for: src/execution/IncrementalGraph.ts

+88-20
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
11
import { BoxedPromiseOrValue } from '../jsutils/BoxedPromiseOrValue.js';
22
import { invariant } from '../jsutils/invariant.js';
33
import { isPromise } from '../jsutils/isPromise.js';
4+
import type { Path } from '../jsutils/Path.js';
45
import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';
56

67
import type { GraphQLError } from '../error/GraphQLError.js';
78

9+
import type { DeferUsage } from './collectFields.js';
810
import type {
911
DeferredFragmentRecord,
1012
DeliveryGroup,
13+
} from './DeferredFragments.js';
14+
import {
15+
DeferredFragmentFactory,
16+
isDeferredFragmentRecord,
17+
} from './DeferredFragments.js';
18+
import type {
1119
IncrementalDataRecord,
1220
IncrementalDataRecordResult,
1321
PendingExecutionGroup,
1422
StreamItemRecord,
1523
StreamRecord,
1624
SuccessfulExecutionGroup,
1725
} from './types.js';
18-
import { isDeferredFragmentRecord, isPendingExecutionGroup } from './types.js';
26+
import { isPendingExecutionGroup } from './types.js';
1927

2028
/**
2129
* @internal
2230
*/
2331
export class IncrementalGraph {
2432
private _rootNodes: Set<DeliveryGroup>;
25-
33+
private _deferredFragmentFactory: DeferredFragmentFactory;
2634
private _completedQueue: Array<IncrementalDataRecordResult>;
2735
private _nextQueue: Array<
2836
(iterable: Iterable<IncrementalDataRecordResult> | undefined) => void
2937
>;
3038

3139
constructor() {
40+
this._deferredFragmentFactory = new DeferredFragmentFactory();
3241
this._rootNodes = new Set();
3342
this._completedQueue = [];
3443
this._nextQueue = [];
@@ -51,11 +60,15 @@ export class IncrementalGraph {
5160
): void {
5261
const { pendingExecutionGroup, incrementalDataRecords } =
5362
successfulExecutionGroup;
63+
const { deferUsages, path } = pendingExecutionGroup;
5464

55-
const deferredFragmentRecords =
56-
pendingExecutionGroup.deferredFragmentRecords;
57-
58-
for (const deferredFragmentRecord of deferredFragmentRecords) {
65+
const deferredFragmentRecords: Array<DeferredFragmentRecord> = [];
66+
for (const deferUsage of deferUsages) {
67+
const deferredFragmentRecord = this._deferredFragmentFactory.get(
68+
deferUsage,
69+
path,
70+
);
71+
deferredFragmentRecords.push(deferredFragmentRecord);
5972
const { pendingExecutionGroups, successfulExecutionGroups } =
6073
deferredFragmentRecord;
6174
pendingExecutionGroups.delete(pendingExecutionGroup);
@@ -70,6 +83,26 @@ export class IncrementalGraph {
7083
}
7184
}
7285

86+
getDeepestDeferredFragmentAtRoot(
87+
initialDeferUsage: DeferUsage,
88+
deferUsages: ReadonlySet<DeferUsage>,
89+
path: Path | undefined,
90+
): DeferredFragmentRecord {
91+
let bestDeferUsage = initialDeferUsage;
92+
let maxDepth = initialDeferUsage.depth;
93+
for (const deferUsage of deferUsages) {
94+
if (deferUsage === initialDeferUsage) {
95+
continue;
96+
}
97+
const depth = deferUsage.depth;
98+
if (depth > maxDepth) {
99+
maxDepth = depth;
100+
bestDeferUsage = deferUsage;
101+
}
102+
}
103+
return this._deferredFragmentFactory.get(bestDeferUsage, path);
104+
}
105+
73106
*currentCompletedBatch(): Generator<IncrementalDataRecordResult> {
74107
let completed;
75108
while ((completed = this._completedQueue.shift()) !== undefined) {
@@ -102,12 +135,20 @@ export class IncrementalGraph {
102135
return this._rootNodes.size > 0;
103136
}
104137

105-
completeDeferredFragment(deferredFragmentRecord: DeferredFragmentRecord):
138+
completeDeferredFragment(
139+
deferUsage: DeferUsage,
140+
path: Path | undefined,
141+
):
106142
| {
143+
deferredFragmentRecord: DeferredFragmentRecord;
107144
newRootNodes: ReadonlyArray<DeliveryGroup>;
108145
successfulExecutionGroups: ReadonlyArray<SuccessfulExecutionGroup>;
109146
}
110147
| undefined {
148+
const deferredFragmentRecord = this._deferredFragmentFactory.get(
149+
deferUsage,
150+
path,
151+
);
111152
if (
112153
!this._rootNodes.has(deferredFragmentRecord) ||
113154
deferredFragmentRecord.pendingExecutionGroups.size > 0
@@ -119,8 +160,13 @@ export class IncrementalGraph {
119160
);
120161
this._rootNodes.delete(deferredFragmentRecord);
121162
for (const successfulExecutionGroup of successfulExecutionGroups) {
122-
for (const otherDeferredFragmentRecord of successfulExecutionGroup
123-
.pendingExecutionGroup.deferredFragmentRecords) {
163+
const { deferUsages, path: resultPath } =
164+
successfulExecutionGroup.pendingExecutionGroup;
165+
for (const otherDeferUsage of deferUsages) {
166+
const otherDeferredFragmentRecord = this._deferredFragmentFactory.get(
167+
otherDeferUsage,
168+
resultPath,
169+
);
124170
otherDeferredFragmentRecord.successfulExecutionGroups.delete(
125171
successfulExecutionGroup,
126172
);
@@ -129,17 +175,22 @@ export class IncrementalGraph {
129175
const newRootNodes = this._promoteNonEmptyToRoot(
130176
deferredFragmentRecord.children,
131177
);
132-
return { newRootNodes, successfulExecutionGroups };
178+
return { deferredFragmentRecord, newRootNodes, successfulExecutionGroups };
133179
}
134180

135181
removeDeferredFragment(
136-
deferredFragmentRecord: DeferredFragmentRecord,
137-
): boolean {
182+
deferUsage: DeferUsage,
183+
path: Path | undefined,
184+
): DeferredFragmentRecord | undefined {
185+
const deferredFragmentRecord = this._deferredFragmentFactory.get(
186+
deferUsage,
187+
path,
188+
);
138189
if (!this._rootNodes.has(deferredFragmentRecord)) {
139-
return false;
190+
return;
140191
}
141192
this._rootNodes.delete(deferredFragmentRecord);
142-
return true;
193+
return deferredFragmentRecord;
143194
}
144195

145196
removeStream(streamRecord: StreamRecord): void {
@@ -153,7 +204,12 @@ export class IncrementalGraph {
153204
): void {
154205
for (const incrementalDataRecord of incrementalDataRecords) {
155206
if (isPendingExecutionGroup(incrementalDataRecord)) {
156-
for (const deferredFragmentRecord of incrementalDataRecord.deferredFragmentRecords) {
207+
const { deferUsages, path } = incrementalDataRecord;
208+
for (const deferUsage of deferUsages) {
209+
const deferredFragmentRecord = this._deferredFragmentFactory.get(
210+
deferUsage,
211+
path,
212+
);
157213
this._addDeferredFragment(
158214
deferredFragmentRecord,
159215
initialResultChildren,
@@ -210,9 +266,17 @@ export class IncrementalGraph {
210266
private _completesRootNode(
211267
pendingExecutionGroup: PendingExecutionGroup,
212268
): boolean {
213-
return pendingExecutionGroup.deferredFragmentRecords.some(
214-
(deferredFragmentRecord) => this._rootNodes.has(deferredFragmentRecord),
215-
);
269+
const { deferUsages, path } = pendingExecutionGroup;
270+
for (const deferUsage of deferUsages) {
271+
const deferredFragmentRecord = this._deferredFragmentFactory.get(
272+
deferUsage,
273+
path,
274+
);
275+
if (this._rootNodes.has(deferredFragmentRecord)) {
276+
return true;
277+
}
278+
}
279+
return false;
216280
}
217281

218282
private _addDeferredFragment(
@@ -222,12 +286,16 @@ export class IncrementalGraph {
222286
if (this._rootNodes.has(deferredFragmentRecord)) {
223287
return;
224288
}
225-
const parent = deferredFragmentRecord.parent;
226-
if (parent === undefined) {
289+
const parentDeferUsage = deferredFragmentRecord.parentDeferUsage;
290+
if (parentDeferUsage === undefined) {
227291
invariant(initialResultChildren !== undefined);
228292
initialResultChildren.add(deferredFragmentRecord);
229293
return;
230294
}
295+
const parent = this._deferredFragmentFactory.get(
296+
parentDeferUsage,
297+
deferredFragmentRecord.path,
298+
);
231299
parent.children.add(deferredFragmentRecord);
232300
this._addDeferredFragment(parent, initialResultChildren);
233301
}

0 commit comments

Comments
 (0)