Skip to content

Commit 7cd62a2

Browse files
authored
perf: reduce notifications during first-load (#9673)
* perf: reduce notifications during first-load * perf: silence more notifications * cleanup impl
1 parent 3ddc965 commit 7cd62a2

File tree

19 files changed

+213
-82
lines changed

19 files changed

+213
-82
lines changed

packages/build-config/src/debugging.ts

+8
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,11 @@ export const LOG_GRAPH: boolean = false;
103103
* @public
104104
*/
105105
export const LOG_INSTANCE_CACHE: boolean = false;
106+
/**
107+
* Log key count metrics, useful for performance
108+
* debugging.
109+
*
110+
* @property {boolean} LOG_METRIC_COUNTS
111+
* @public
112+
*/
113+
export const LOG_METRIC_COUNTS: boolean = false;

packages/graph/src/-private/-diff.ts

-4
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,6 @@ export function _addLocal(
298298
relationship.localState.push(value);
299299
}
300300
}
301-
assert(
302-
`Expected relationship to be dirty when adding a local mutation`,
303-
relationship.localState || relationship.isDirty
304-
);
305301

306302
return true;
307303
}

packages/graph/src/-private/-utils.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export function removeIdentifierCompletelyFromRelationship(
143143
// we shouldn't be notifying here though, figure out where
144144
// a notification was missed elsewhere.
145145
if (!silenceNotifications) {
146-
notifyChange(graph, relationship.identifier, relationship.definition.key);
146+
notifyChange(graph, relationship);
147147
}
148148
}
149149
} else if (isHasMany(relationship)) {
@@ -164,7 +164,7 @@ export function removeIdentifierCompletelyFromRelationship(
164164
// we shouldn't be notifying here though, figure out where
165165
// a notification was missed elsewhere.
166166
if (!silenceNotifications) {
167-
notifyChange(graph, relationship.identifier, relationship.definition.key);
167+
notifyChange(graph, relationship);
168168
}
169169
}
170170
}
@@ -174,8 +174,14 @@ export function removeIdentifierCompletelyFromRelationship(
174174
}
175175
}
176176

177-
// TODO add silencing at the graph level
178-
export function notifyChange(graph: Graph, identifier: StableRecordIdentifier, key: string) {
177+
export function notifyChange(graph: Graph, relationship: CollectionEdge | ResourceEdge): void {
178+
if (!relationship.accessed) {
179+
return;
180+
}
181+
182+
const identifier = relationship.identifier;
183+
const key = relationship.definition.key;
184+
179185
if (identifier === graph._removing) {
180186
if (LOG_GRAPH) {
181187
// eslint-disable-next-line no-console

packages/graph/src/-private/edges/collection.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,24 @@ export interface CollectionEdge {
2222
links: Links | PaginationLinks | null;
2323

2424
localState: StableRecordIdentifier[] | null;
25+
/**
26+
* Whether the localState for this edge is out-of-sync
27+
* with the remoteState.
28+
*
29+
* if state.hasReceivedData=false we are also
30+
* not dirty since there is nothing to sync with.
31+
*
32+
* @typedoc
33+
*/
2534
isDirty: boolean;
2635
transactionRef: number;
36+
/**
37+
* Whether data for this edge has been accessed at least once
38+
* via `graph.getData`
39+
*
40+
* @typedoc
41+
*/
42+
accessed: boolean;
2743

2844
_diff?: {
2945
add: Set<StableRecordIdentifier>;
@@ -45,13 +61,15 @@ export function createCollectionEdge(definition: UpgradedMeta, identifier: Stabl
4561
links: null,
4662

4763
localState: null,
48-
isDirty: true,
64+
isDirty: false,
4965
transactionRef: 0,
66+
accessed: false,
5067
_diff: undefined,
5168
};
5269
}
5370

5471
export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship {
72+
source.accessed = true;
5573
const payload: CollectionRelationship = {};
5674

5775
if (source.state.hasReceivedData) {

packages/graph/src/-private/edges/resource.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ResourceEdge {
2323
meta: Meta | null;
2424
links: Links | PaginationLinks | null;
2525
transactionRef: number;
26+
accessed: boolean;
2627
}
2728

2829
export function createResourceEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): ResourceEdge {
@@ -35,10 +36,12 @@ export function createResourceEdge(definition: UpgradedMeta, identifier: StableR
3536
remoteState: null,
3637
meta: null,
3738
links: null,
39+
accessed: false,
3840
};
3941
}
4042

4143
export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship {
44+
source.accessed = true;
4245
let data: StableRecordIdentifier | null | undefined;
4346
const payload: ResourceRelationship = {};
4447
if (source.localState) {

packages/graph/src/-private/graph.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export class Graph {
559559
this._willSyncLocal = false;
560560
const updated = this._updatedRelationships;
561561
this._updatedRelationships = new Set();
562-
updated.forEach((rel) => notifyChange(this, rel.identifier, rel.definition.key));
562+
updated.forEach((rel) => notifyChange(this, rel));
563563
}
564564

565565
destroy() {
@@ -641,7 +641,7 @@ function destroyRelationship(graph: Graph, rel: GraphEdge, silenceNotifications?
641641
// leave the ui relationship populated since the record is destroyed and
642642
// internally we've fully cleaned up.
643643
if (!rel.definition.isAsync && !silenceNotifications) {
644-
/*#__NOINLINE__*/ notifyChange(graph, rel.identifier, rel.definition.key);
644+
/*#__NOINLINE__*/ notifyChange(graph, rel);
645645
}
646646
}
647647
}
@@ -713,7 +713,7 @@ function removeDematerializedInverse(
713713
}
714714

715715
if (!silenceNotifications) {
716-
notifyChange(graph, relationship.identifier, relationship.definition.key);
716+
notifyChange(graph, relationship);
717717
}
718718
} else {
719719
if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) {
@@ -728,7 +728,7 @@ function removeDematerializedInverse(
728728
}
729729

730730
if (!silenceNotifications) {
731-
notifyChange(graph, relationship.identifier, relationship.definition.key);
731+
notifyChange(graph, relationship);
732732
}
733733
}
734734
}
@@ -753,7 +753,7 @@ function removeCompletelyFromInverse(graph: Graph, relationship: GraphEdge) {
753753
if (!relationship.definition.isAsync) {
754754
clearRelationship(relationship);
755755

756-
notifyChange(graph, relationship.identifier, relationship.definition.key);
756+
notifyChange(graph, relationship);
757757
}
758758
} else {
759759
relationship.remoteMembers.clear();

packages/graph/src/-private/operations/add-to-related-records.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord
1919
`You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`,
2020
isHasMany(relationship)
2121
);
22+
23+
// if we are not dirty but have a null localState then we
24+
// are mutating a relationship that has never been fetched
25+
// so we initialize localState to an empty array
26+
if (!relationship.isDirty && !relationship.localState) {
27+
relationship.localState = [];
28+
}
2229
if (Array.isArray(value)) {
2330
for (let i = 0; i < value.length; i++) {
2431
addRelatedRecord(graph, relationship, record, value[i], index !== undefined ? index + i : index, isRemote);
@@ -27,7 +34,7 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord
2734
addRelatedRecord(graph, relationship, record, value, index, isRemote);
2835
}
2936

30-
notifyChange(graph, relationship.identifier, relationship.definition.key);
37+
notifyChange(graph, relationship);
3138
}
3239

3340
function addRelatedRecord(

packages/graph/src/-private/operations/merge-identifier.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function mergeBelongsTo(graph: Graph, rel: ResourceEdge, op: MergeOperation): vo
4040
}
4141
if (rel.localState === op.record) {
4242
rel.localState = op.value;
43-
notifyChange(graph, rel.identifier, rel.definition.key);
43+
notifyChange(graph, rel);
4444
}
4545
}
4646

@@ -63,7 +63,7 @@ function mergeHasMany(graph: Graph, rel: CollectionEdge, op: MergeOperation): vo
6363
rel.isDirty = true;
6464
}
6565
if (rel.isDirty) {
66-
notifyChange(graph, rel.identifier, rel.definition.key);
66+
notifyChange(graph, rel);
6767
}
6868
}
6969

packages/graph/src/-private/operations/remove-from-related-records.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function removeFromRelatedRecords(graph: Graph, op: RemoveFromRel
3030
} else {
3131
removeRelatedRecord(graph, relationship, record, value, isRemote);
3232
}
33-
notifyChange(graph, relationship.identifier, relationship.definition.key);
33+
notifyChange(graph, relationship);
3434
}
3535

3636
function removeRelatedRecord(

packages/graph/src/-private/operations/replace-related-record.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec
107107
}
108108
);
109109

110-
notifyChange(graph, relationship.identifier, relationship.definition.key);
110+
notifyChange(graph, relationship);
111111
}
112112
}
113113
}
@@ -156,7 +156,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec
156156
// and we can safely sync the new remoteState to local
157157
if (localState !== remoteState && localState === existingState) {
158158
relationship.localState = remoteState;
159-
notifyChange(graph, relationship.identifier, relationship.definition.key);
159+
notifyChange(graph, relationship);
160160
// But when localState does not match the new remoteState and
161161
// and localState !== existingState then we know we have a local mutation
162162
// that has not been persisted yet.
@@ -186,10 +186,10 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec
186186
}
187187
);
188188

189-
notifyChange(graph, relationship.identifier, relationship.definition.key);
189+
notifyChange(graph, relationship);
190190
}
191191
}
192192
} else {
193-
notifyChange(graph, relationship.identifier, relationship.definition.key);
193+
notifyChange(graph, relationship);
194194
}
195195
}

packages/graph/src/-private/operations/replace-related-records.ts

+30-25
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,6 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera
8080
const relationship = graph.get(op.record, op.field);
8181
assert(`expected hasMany relationship`, isHasMany(relationship));
8282

83-
// relationships for newly created records begin in the dirty state, so if updated
84-
// before flushed we would fail to notify. This check helps us avoid that.
85-
const isMaybeFirstUpdate =
86-
relationship.remoteState.length === 0 &&
87-
relationship.localState === null &&
88-
relationship.state.hasReceivedData === false;
8983
relationship.state.hasReceivedData = true;
9084
const { additions, removals } = relationship;
9185
const { inverseKey, type } = relationship.definition;
@@ -158,13 +152,11 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera
158152
relationship.additions = diff.add;
159153
relationship.removals = diff.del;
160154
relationship.localState = diff.finalState;
161-
relationship.isDirty = wasDirty;
162155

163-
// we notify if this is the first update to the relationship
164-
// because ?? may need to recalculate.
165-
// otherwise we only notify if we are dirty and were not already dirty before
166-
if (isMaybeFirstUpdate || (becameDirty && !wasDirty)) {
167-
notifyChange(graph, op.record, op.field);
156+
// we only notify if the localState changed and were not already dirty before
157+
// because if we were already dirty then we have already notified
158+
if (becameDirty && !wasDirty) {
159+
notifyChange(graph, relationship);
168160
}
169161
}
170162

@@ -180,8 +172,14 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
180172
graph._addToTransaction(relationship);
181173
}
182174

183-
// see note before flushCanonical
184-
// const wasDirty = relationship.isDirty;
175+
const wasDirty = relationship.isDirty;
176+
// if this is our first time receiving data
177+
// we need to mark the relationship as dirty
178+
// so that non-materializing APIs like `hasManyReference.value()`
179+
// will get notified and updated.
180+
if (!relationship.state.hasReceivedData) {
181+
relationship.isDirty = true;
182+
}
185183
relationship.state.hasReceivedData = true;
186184

187185
// cache existing state
@@ -310,10 +308,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
310308
}
311309
}
312310

313-
// we ought to only flush if we became dirty and were not before
314-
// but this causes a fw test failures around unloadRecord and reference autotracking
315-
// we should investigate this further
316-
if (relationship.isDirty /*&& !wasDirty*/) {
311+
if (relationship.isDirty && !wasDirty) {
317312
flushCanonical(graph, relationship);
318313
}
319314
}
@@ -352,7 +347,8 @@ export function addToInverse(
352347
removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, identifier, isRemote);
353348
}
354349
relationship.localState = value;
355-
notifyChange(graph, identifier, key);
350+
351+
notifyChange(graph, relationship);
356352
}
357353
} else if (isHasMany(relationship)) {
358354
if (isRemote) {
@@ -374,8 +370,15 @@ export function addToInverse(
374370
}
375371
}
376372
} else {
373+
// if we are not dirty but have a null localState then we
374+
// are mutating a relationship that has never been fetched
375+
// so we initialize localState to an empty array
376+
if (!relationship.isDirty && !relationship.localState) {
377+
relationship.localState = [];
378+
}
379+
377380
if (_addLocal(graph, identifier, relationship, value, null)) {
378-
notifyChange(graph, identifier, key);
381+
notifyChange(graph, relationship);
379382
}
380383
}
381384
} else {
@@ -401,7 +404,7 @@ export function notifyInverseOfPotentialMaterialization(
401404
) {
402405
const relationship = graph.get(identifier, key);
403406
if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) {
404-
notifyChange(graph, identifier, key);
407+
notifyChange(graph, relationship);
405408
}
406409
}
407410

@@ -423,17 +426,17 @@ export function removeFromInverse(
423426
if (relationship.localState === value) {
424427
relationship.localState = null;
425428

426-
notifyChange(graph, identifier, key);
429+
notifyChange(graph, relationship);
427430
}
428431
} else if (isHasMany(relationship)) {
429432
if (isRemote) {
430433
graph._addToTransaction(relationship);
431434
if (_removeRemote(relationship, value)) {
432-
notifyChange(graph, identifier, key);
435+
notifyChange(graph, relationship);
433436
}
434437
} else {
435438
if (_removeLocal(relationship, value)) {
436-
notifyChange(graph, identifier, key);
439+
notifyChange(graph, relationship);
437440
}
438441
}
439442
} else {
@@ -449,5 +452,7 @@ export function removeFromInverse(
449452
}
450453

451454
function flushCanonical(graph: Graph, rel: CollectionEdge) {
452-
graph._scheduleLocalSync(rel);
455+
if (rel.accessed) {
456+
graph._scheduleLocalSync(rel);
457+
}
453458
}

packages/graph/src/-private/operations/update-relationship.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela
157157
) {
158158
relationship.state.isStale = true;
159159

160-
notifyChange(graph, relationship.identifier, relationship.definition.key);
160+
notifyChange(graph, relationship);
161161
} else {
162162
relationship.state.isStale = false;
163163
}

0 commit comments

Comments
 (0)