Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various cache read and write performance optimizations. #5948

Merged
merged 11 commits into from
Feb 16, 2020
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
These local variables are _reactive_ in the sense that updating their values invalidates any previously cached query results that depended on the old values. <br/>
[@benjamn](https://github.com/benjamn) in [#5799](https://github.com/apollographql/apollo-client/pull/5799)

- Various cache read and write performance optimizations, cutting read and write times by more than 50% in larger benchmarks. <br/>
[@benjamn](https://github.com/benjamn) in [#5948](https://github.com/apollographql/apollo-client/pull/5948)

- The `cache.readQuery` and `cache.writeQuery` methods now accept an `options.id` string, which eliminates most use cases for `cache.readFragment` and `cache.writeFragment`, and skips the implicit conversion of fragment documents to query documents performed by `cache.{read,write}Fragment`. <br/>
[@benjamn](https://github.com/benjamn) in [#5930](https://github.com/apollographql/apollo-client/pull/5930)

Expand Down
5 changes: 4 additions & 1 deletion src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ class CacheGroup {
}

function makeDepKey(dataId: string, storeFieldName: string) {
return JSON.stringify([dataId, fieldNameFromStoreName(storeFieldName)]);
// Since field names cannot have newline characters in them, this method
// of joining the field name and the ID should be unambiguous, and much
// cheaper than JSON.stringify([dataId, fieldName]).
return fieldNameFromStoreName(storeFieldName) + "#" + dataId;
}

export namespace EntityStore {
Expand Down
19 changes: 10 additions & 9 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,8 @@ export class Policies {
};
} = Object.create(null);

public readonly rootTypenamesById: Readonly<Record<string, string>> = {
__proto__: null, // Equivalent to Object.create(null)
ROOT_QUERY: "Query",
ROOT_MUTATION: "Mutation",
ROOT_SUBSCRIPTION: "Subscription",
};
public readonly rootIdsByTypename: Record<string, string> = Object.create(null);
public readonly rootTypenamesById: Record<string, string> = Object.create(null);

public readonly usingPossibleTypes = false;

Expand All @@ -231,6 +227,10 @@ export class Policies {
...config,
};

this.setRootTypename("Query");
this.setRootTypename("Mutation");
this.setRootTypename("Subscription");

if (config.possibleTypes) {
this.addPossibleTypes(config.possibleTypes);
}
Expand Down Expand Up @@ -346,13 +346,14 @@ export class Policies {

private setRootTypename(
which: "Query" | "Mutation" | "Subscription",
typename: string,
typename: string = which,
) {
const rootId = "ROOT_" + which.toUpperCase();
const old = this.rootTypenamesById[rootId];
if (typename !== old) {
invariant(old === which, `Cannot change root ${which} __typename more than once`);
(this.rootTypenamesById as any)[rootId] = typename;
invariant(!old || old === which, `Cannot change root ${which} __typename more than once`);
this.rootIdsByTypename[typename] = rootId;
this.rootTypenamesById[rootId] = typename;
}
}

Expand Down
33 changes: 16 additions & 17 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ interface ExecContext {
policies: Policies;
fragmentMap: FragmentMap;
variables: VariableMap;
// A JSON.stringify-serialized version of context.variables.
varString: string;
};

export type ExecResult<R = any> = {
Expand Down Expand Up @@ -91,7 +93,7 @@ export class StoreReader {
if (supportsResultCaching(context.store)) {
return context.store.makeCacheKey(
selectionSet,
JSON.stringify(context.variables),
context.varString,
isReference(objectOrReference)
? objectOrReference.__ref
: objectOrReference,
Expand All @@ -108,7 +110,7 @@ export class StoreReader {
return context.store.makeCacheKey(
field,
array,
JSON.stringify(context.variables),
context.varString,
);
}
}
Expand Down Expand Up @@ -151,17 +153,20 @@ export class StoreReader {
}: DiffQueryAgainstStoreOptions): Cache.DiffResult<T> {
const { policies } = this.config;

variables = {
...getDefaultValues(getQueryDefinition(query)),
...variables,
};

const execResult = this.executeSelectionSet({
selectionSet: getMainDefinition(query).selectionSet,
objectOrReference: makeReference(rootId),
context: {
store,
query,
policies,
variables: {
...getDefaultValues(getQueryDefinition(query)),
...variables,
},
variables,
varString: JSON.stringify(variables),
fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
},
});
Expand Down Expand Up @@ -201,9 +206,7 @@ export class StoreReader {

if (this.config.addTypename &&
typeof typename === "string" &&
Object.values(
policies.rootTypenamesById
).indexOf(typename) < 0) {
!policies.rootIdsByTypename[typename]) {
// Ensure we always include a default value for the __typename
// field, if we have one, and this.config.addTypename is true. Note
// that this field can be overridden by other merged objects.
Expand All @@ -219,7 +222,9 @@ export class StoreReader {
return result.result;
}

selectionSet.selections.forEach(selection => {
const workSet = new Set(selectionSet.selections);

workSet.forEach(selection => {
// Omit fields with directives @skip(if: <truthy value>) or
// @include(if: <falsy value>).
if (!shouldInclude(selection, variables)) return;
Expand Down Expand Up @@ -296,13 +301,7 @@ export class StoreReader {
}

if (policies.fragmentMatches(fragment, typename)) {
objectsToMerge.push(handleMissing(
this.executeSelectionSet({
selectionSet: fragment.selectionSet,
objectOrReference,
context,
})
));
fragment.selectionSet.selections.forEach(workSet.add, workSet);
}
}
});
Expand Down
95 changes: 54 additions & 41 deletions src/cache/inmemory/writeToStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { cloneDeep } from '../../utilities/common/cloneDeep';
import { Policies } from './policies';
import { defaultNormalizedCacheFactory } from './entityStore';
import { NormalizedCache, StoreObject } from './types';
import { makeProcessedFieldsMerger } from './helpers';
import { makeProcessedFieldsMerger, FieldValueToBeMerged } from './helpers';

export type WriteContext = {
readonly store: NormalizedCache;
Expand All @@ -40,6 +40,16 @@ export type WriteContext = {
merge<T>(existing: T, incoming: T): T;
};

interface ProcessSelectionSetOptions {
result: Record<string, any>;
selectionSet: SelectionSetNode;
context: WriteContext;
typename: string;
out: {
shouldApplyMerges?: boolean;
};
}

export interface StoreWriterConfig {
policies: Policies;
};
Expand Down Expand Up @@ -134,20 +144,26 @@ export class StoreWriter {
// fall back to that.
store.get(dataId, "__typename") as string;

store.merge(
dataId,
policies.applyMerges(
const out: ProcessSelectionSetOptions["out"] = Object.create(null);

let processed = this.processSelectionSet({
result,
selectionSet,
context,
typename,
out,
});

if (out.shouldApplyMerges) {
processed = policies.applyMerges(
makeReference(dataId),
this.processSelectionSet({
result,
selectionSet,
context,
typename,
}),
processed,
store.getFieldValue,
context.variables,
),
);
);
}

store.merge(dataId, processed);

return store;
}
Expand All @@ -157,23 +173,20 @@ export class StoreWriter {
selectionSet,
context,
typename,
}: {
result: Record<string, any>;
selectionSet: SelectionSetNode;
context: WriteContext;
typename: string;
}): StoreObject {
// This object allows processSelectionSet to report useful information
// to its callers without explicitly returning that information.
out,
}: ProcessSelectionSetOptions): StoreObject {
let mergedFields: StoreObject = Object.create(null);
if (typeof typename === "string") {
mergedFields.__typename = typename;
}

selectionSet.selections.forEach(selection => {
if (!shouldInclude(selection, context.variables)) {
return;
}
const { policies } = this;
const workSet = new Set(selectionSet.selections);

const { policies } = this;
workSet.forEach(selection => {
if (!shouldInclude(selection, context.variables)) return;

if (isField(selection)) {
const resultFieldKey = resultKeyNameFromField(selection);
Expand All @@ -186,22 +199,27 @@ export class StoreWriter {
context.variables,
);

const incomingValue =
this.processFieldValue(value, selection, context);
let incomingValue =
this.processFieldValue(value, selection, context, out);

mergedFields = context.merge(mergedFields, {
if (policies.hasMergeFunction(typename, selection.name.value)) {
// If a custom merge function is defined for this field, store
// a special FieldValueToBeMerged object, so that we can run
// the merge function later, after all processSelectionSet
// work is finished.
[storeFieldName]: policies.hasMergeFunction(
typename,
selection.name.value,
) ? {
incomingValue = {
__field: selection,
__typename: typename,
__value: incomingValue,
} : incomingValue,
} as FieldValueToBeMerged;

// Communicate to the caller that mergedFields contains at
// least one FieldValueToBeMerged.
out.shouldApplyMerges = true;
}

mergedFields = context.merge(mergedFields, {
[storeFieldName]: incomingValue,
});

} else if (
Expand Down Expand Up @@ -233,15 +251,7 @@ export class StoreWriter {
);

if (policies.fragmentMatches(fragment, typename)) {
mergedFields = context.merge(
mergedFields,
this.processSelectionSet({
result,
selectionSet: fragment.selectionSet,
context,
typename,
}),
);
fragment.selectionSet.selections.forEach(workSet.add, workSet);
}
}
});
Expand All @@ -253,6 +263,7 @@ export class StoreWriter {
value: any,
field: FieldNode,
context: WriteContext,
out: ProcessSelectionSetOptions["out"],
): StoreValue {
if (!field.selectionSet || value === null) {
// In development, we need to clone scalar values so that they can be
Expand All @@ -262,7 +273,8 @@ export class StoreWriter {
}

if (Array.isArray(value)) {
return value.map((item, i) => this.processFieldValue(item, field, context));
return value.map(
(item, i) => this.processFieldValue(item, field, context, out));
}

if (value) {
Expand Down Expand Up @@ -291,6 +303,7 @@ export class StoreWriter {
context,
typename: getTypenameFromResult(
value, field.selectionSet, context.fragmentMap),
out,
});
}
}
12 changes: 4 additions & 8 deletions src/utilities/common/mergeDeep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ const defaultReconciler: ReconcilerFunction<any[]> =
};

export class DeepMerger<TContextArgs extends any[]> {
private pastCopies: any[] = [];

constructor(
private reconciler: ReconcilerFunction<TContextArgs> = defaultReconciler,
) {}
Expand Down Expand Up @@ -101,12 +99,10 @@ export class DeepMerger<TContextArgs extends any[]> {

public isObject = isObject;

private pastCopies = new Set<any>();

public shallowCopyForMerge<T>(value: T): T {
if (
value !== null &&
typeof value === 'object' &&
this.pastCopies.indexOf(value) < 0
) {
if (isObject(value) && !this.pastCopies.has(value)) {
if (Array.isArray(value)) {
value = (value as any).slice(0);
} else {
Expand All @@ -115,7 +111,7 @@ export class DeepMerger<TContextArgs extends any[]> {
...value,
};
}
this.pastCopies.push(value);
this.pastCopies.add(value);
}
return value;
}
Expand Down
Loading