Skip to content

Commit

Permalink
Merge pull request #5948 from apollographql/read-and-write-performanc…
Browse files Browse the repository at this point in the history
…e-optimizations

Various cache read and write performance optimizations.
  • Loading branch information
benjamn committed Feb 16, 2020
2 parents 7c89a0d + 0a4ae81 commit b094e30
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 106 deletions.
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

0 comments on commit b094e30

Please sign in to comment.