Skip to content

Commit

Permalink
experiment(incremental): lazily create DeferredFragments
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yaacovCR committed Jul 25, 2024
1 parent 02ee34f commit b5dfb51
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 278 deletions.
185 changes: 156 additions & 29 deletions src/execution/IncrementalGraph.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { BoxedPromiseOrValue } from '../jsutils/BoxedPromiseOrValue.js';
import { invariant } from '../jsutils/invariant.js';
import { isPromise } from '../jsutils/isPromise.js';
import type { Path } from '../jsutils/Path.js';
import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';

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

import type { DeferUsage } from './collectFields.js';
import type {
DeferredFragmentRecord,
DeliveryGroup,
IncrementalDataRecord,
IncrementalDataRecordResult,
Expand All @@ -15,20 +16,88 @@ import type {
StreamRecord,
SuccessfulExecutionGroup,
} from './types.js';
import { isDeferredFragmentRecord, isPendingExecutionGroup } from './types.js';
import {
DeferredFragmentRecord,
isDeferredFragmentRecord,
isPendingExecutionGroup,
} from './types.js';

/**
* @internal
*/
class DeferredFragmentFactory {
private _rootDeferredFragments = new Map<
DeferUsage,
DeferredFragmentRecord
>();

get(deferUsage: DeferUsage, path: Path | undefined): DeferredFragmentRecord {
const deferUsagePath = this._pathAtDepth(path, deferUsage.depth);
let deferredFragmentRecords:
| Map<DeferUsage, DeferredFragmentRecord>
| undefined;
if (deferUsagePath === undefined) {
deferredFragmentRecords = this._rootDeferredFragments;
} else {
deferredFragmentRecords = (
deferUsagePath as unknown as {
deferredFragmentRecords: Map<DeferUsage, DeferredFragmentRecord>;
}
).deferredFragmentRecords;
if (deferredFragmentRecords === undefined) {
deferredFragmentRecords = new Map();
(
deferUsagePath as unknown as {
deferredFragmentRecords: Map<DeferUsage, DeferredFragmentRecord>;
}
).deferredFragmentRecords = deferredFragmentRecords;
}
}
let deferredFragmentRecord = deferredFragmentRecords.get(deferUsage);
if (deferredFragmentRecord === undefined) {
const { label, parentDeferUsage } = deferUsage;
deferredFragmentRecord = new DeferredFragmentRecord(
deferUsagePath,
label,
parentDeferUsage,
);
deferredFragmentRecords.set(deferUsage, deferredFragmentRecord);
}
return deferredFragmentRecord;
}

private _pathAtDepth(
path: Path | undefined,
depth: number,
): Path | undefined {
if (depth === 0) {
return;
}
let currentPath = path;
invariant(currentPath !== undefined);
let currentDepth = currentPath.depth;
while (currentDepth > depth) {
currentPath = currentPath.prev;
invariant(currentPath !== undefined);
currentDepth = currentPath.depth;
}
return currentPath;
}
}

/**
* @internal
*/
export class IncrementalGraph {
private _rootNodes: Set<DeliveryGroup>;

private _deferredFragmentFactory: DeferredFragmentFactory;
private _completedQueue: Array<IncrementalDataRecordResult>;
private _nextQueue: Array<
(iterable: Iterable<IncrementalDataRecordResult> | undefined) => void
>;

constructor() {
this._deferredFragmentFactory = new DeferredFragmentFactory();
this._rootNodes = new Set();
this._completedQueue = [];
this._nextQueue = [];
Expand All @@ -49,8 +118,17 @@ export class IncrementalGraph {
addCompletedSuccessfulExecutionGroup(
successfulExecutionGroup: SuccessfulExecutionGroup,
): void {
for (const deferredFragmentRecord of successfulExecutionGroup
.pendingExecutionGroup.deferredFragmentRecords) {
const { pendingExecutionGroup, incrementalDataRecords } =
successfulExecutionGroup;
const { deferUsages, path } = pendingExecutionGroup;

const deferredFragmentRecords: Array<DeferredFragmentRecord> = [];
for (const deferUsage of deferUsages) {
const deferredFragmentRecord = this._deferredFragmentFactory.get(
deferUsage,
path,
);
deferredFragmentRecords.push(deferredFragmentRecord);
deferredFragmentRecord.pendingExecutionGroups.delete(
successfulExecutionGroup.pendingExecutionGroup,
);
Expand All @@ -59,16 +137,34 @@ export class IncrementalGraph {
);
}

const incrementalDataRecords =
successfulExecutionGroup.incrementalDataRecords;
if (incrementalDataRecords !== undefined) {
this._addIncrementalDataRecords(
incrementalDataRecords,
successfulExecutionGroup.pendingExecutionGroup.deferredFragmentRecords,
deferredFragmentRecords,
);
}
}

getDeepestDeferredFragmentRecord(
initialDeferUsage: DeferUsage,
deferUsages: ReadonlySet<DeferUsage>,
path: Path | undefined,
): DeferredFragmentRecord {
let bestDeferUsage = initialDeferUsage;
let maxDepth = initialDeferUsage.depth;
for (const deferUsage of deferUsages) {
if (deferUsage === initialDeferUsage) {
continue;
}
const depth = deferUsage.depth;
if (depth > maxDepth) {
maxDepth = depth;
bestDeferUsage = deferUsage;
}
}
return this._deferredFragmentFactory.get(bestDeferUsage, path);
}

*currentCompletedBatch(): Generator<IncrementalDataRecordResult> {
let completed;
while ((completed = this._completedQueue.shift()) !== undefined) {
Expand Down Expand Up @@ -101,12 +197,20 @@ export class IncrementalGraph {
return this._rootNodes.size > 0;
}

completeDeferredFragment(deferredFragmentRecord: DeferredFragmentRecord):
completeDeferredFragment(
deferUsage: DeferUsage,
path: Path | undefined,
):
| {
deferredFragmentRecord: DeferredFragmentRecord;
newRootNodes: ReadonlyArray<DeliveryGroup>;
successfulExecutionGroups: ReadonlyArray<SuccessfulExecutionGroup>;
}
| undefined {
const deferredFragmentRecord = this._deferredFragmentFactory.get(
deferUsage,
path,
);
if (
!this._rootNodes.has(deferredFragmentRecord) ||
deferredFragmentRecord.pendingExecutionGroups.size > 0
Expand All @@ -116,10 +220,15 @@ export class IncrementalGraph {
const successfulExecutionGroups = Array.from(
deferredFragmentRecord.successfulExecutionGroups,
);
this._removeRootNode(deferredFragmentRecord);
this._rootNodes.delete(deferredFragmentRecord);
for (const successfulExecutionGroup of successfulExecutionGroups) {
for (const otherDeferredFragmentRecord of successfulExecutionGroup
.pendingExecutionGroup.deferredFragmentRecords) {
const { deferUsages, path: resultPath } =
successfulExecutionGroup.pendingExecutionGroup;
for (const otherDeferUsage of deferUsages) {
const otherDeferredFragmentRecord = this._deferredFragmentFactory.get(
otherDeferUsage,
resultPath,
);
otherDeferredFragmentRecord.successfulExecutionGroups.delete(
successfulExecutionGroup,
);
Expand All @@ -128,25 +237,26 @@ export class IncrementalGraph {
const newRootNodes = this._promoteNonEmptyToRoot(
deferredFragmentRecord.children,
);
return { newRootNodes, successfulExecutionGroups };
return { deferredFragmentRecord, newRootNodes, successfulExecutionGroups };
}

removeDeferredFragment(
deferredFragmentRecord: DeferredFragmentRecord,
): boolean {
deferUsage: DeferUsage,
path: Path | undefined,
): DeferredFragmentRecord | undefined {
const deferredFragmentRecord = this._deferredFragmentFactory.get(
deferUsage,
path,
);
if (!this._rootNodes.has(deferredFragmentRecord)) {
return false;
return;
}
this._removeRootNode(deferredFragmentRecord);
return true;
this._rootNodes.delete(deferredFragmentRecord);
return deferredFragmentRecord;
}

removeStream(streamRecord: StreamRecord): void {
this._removeRootNode(streamRecord);
}

private _removeRootNode(deliveryGroup: DeliveryGroup): void {
this._rootNodes.delete(deliveryGroup);
this._rootNodes.delete(streamRecord);
}

private _addIncrementalDataRecords(
Expand All @@ -156,7 +266,12 @@ export class IncrementalGraph {
): void {
for (const incrementalDataRecord of incrementalDataRecords) {
if (isPendingExecutionGroup(incrementalDataRecord)) {
for (const deferredFragmentRecord of incrementalDataRecord.deferredFragmentRecords) {
const { deferUsages, path } = incrementalDataRecord;
for (const deferUsage of deferUsages) {
const deferredFragmentRecord = this._deferredFragmentFactory.get(
deferUsage,
path,
);
this._addDeferredFragment(
deferredFragmentRecord,
initialResultChildren,
Expand Down Expand Up @@ -213,9 +328,17 @@ export class IncrementalGraph {
private _completesRootNode(
pendingExecutionGroup: PendingExecutionGroup,
): boolean {
return pendingExecutionGroup.deferredFragmentRecords.some(
(deferredFragmentRecord) => this._rootNodes.has(deferredFragmentRecord),
);
const { deferUsages, path } = pendingExecutionGroup;
for (const deferUsage of deferUsages) {
const deferredFragmentRecord = this._deferredFragmentFactory.get(
deferUsage,
path,
);
if (this._rootNodes.has(deferredFragmentRecord)) {
return true;
}
}
return false;
}

private _addDeferredFragment(
Expand All @@ -225,12 +348,16 @@ export class IncrementalGraph {
if (this._rootNodes.has(deferredFragmentRecord)) {
return;
}
const parent = deferredFragmentRecord.parent;
if (parent === undefined) {
const parentDeferUsage = deferredFragmentRecord.parentDeferUsage;
if (parentDeferUsage === undefined) {
invariant(initialResultChildren !== undefined);
initialResultChildren.add(deferredFragmentRecord);
return;
}
const parent = this._deferredFragmentFactory.get(
parentDeferUsage,
deferredFragmentRecord.path,
);
parent.children.add(deferredFragmentRecord);
this._addDeferredFragment(parent, initialResultChildren);
}
Expand Down
Loading

0 comments on commit b5dfb51

Please sign in to comment.