Skip to content

Commit 499dff2

Browse files
gitKrystanrunspired
andcommitted
Don't overnotify for updates to added state that match local updates (#9702)
* Don't overnotify for updates to added state that match local updates * Fix typo * Tidy up * stash stuffs * Add log counts to replace-related-records * update all the stuffs * fixup log --------- Co-authored-by: Chris Thoburn <runspired@users.noreply.github.com>
1 parent 3121543 commit 499dff2

File tree

2 files changed

+139
-11
lines changed

2 files changed

+139
-11
lines changed

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

+108-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { deprecate } from '@ember/debug';
22

3+
import { DEBUG_RELATIONSHIP_NOTIFICATIONS } from '@warp-drive/build-config/debugging';
34
import {
45
DEPRECATE_NON_UNIQUE_PAYLOADS,
56
DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE,
@@ -53,7 +54,13 @@ function _deprecatedCompare<T>(
5354
adv = true;
5455

5556
if (!prevSet.has(member)) {
56-
changed = true;
57+
// Avoid unnecessarily notifying a change that already exists locally
58+
if (i < priorLocalLength) {
59+
const priorLocalMember = priorLocalState![i];
60+
if (priorLocalMember !== member) {
61+
changed = true;
62+
}
63+
}
5764
added.add(member);
5865
onAdd(member);
5966
}
@@ -158,25 +165,59 @@ function _compare<T>(
158165
const finalLength = finalState.length;
159166
const prevLength = prevState.length;
160167
const iterationLength = Math.max(finalLength, prevLength);
161-
const equalLength = finalLength === prevLength;
162-
let changed: boolean = finalSet.size !== prevSet.size;
163-
let remoteOrderChanged = false;
168+
const equalLength = priorLocalState ? finalLength === priorLocalState.length : finalLength === prevLength;
169+
let remoteOrderChanged = finalSet.size !== prevSet.size;
170+
let changed: boolean = priorLocalState ? finalSet.size !== priorLocalState.length : remoteOrderChanged;
164171
const added = new Set<T>();
165172
const removed = new Set<T>();
166173
const priorLocalLength = priorLocalState?.length ?? 0;
167174

175+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
176+
if (changed) {
177+
// console.log({
178+
// priorState: priorLocalState?.slice(),
179+
// finalState: finalState.slice(),
180+
// prevState: prevState.slice(),
181+
// });
182+
}
183+
184+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
185+
changed &&
186+
// eslint-disable-next-line no-console
187+
console.log(
188+
`changed because ${priorLocalState ? 'finalSet.size !== priorLocalState.length' : 'finalSet.size !== prevSet.size'}`
189+
);
190+
}
191+
168192
for (let i = 0; i < iterationLength; i++) {
169193
let member: T | undefined;
170194

171195
// accumulate anything added
172196
if (i < finalLength) {
173197
member = finalState[i];
174198
if (!prevSet.has(member)) {
175-
// TODO: in order to avoid unnecessarily notifying a change here
176-
// we would need to only notify "changed" if member is not in
177-
// relationship.additions OR if localState[i] !== member
178-
179-
changed = true;
199+
// Avoid unnecessarily notifying a change that already exists locally
200+
if (i < priorLocalLength) {
201+
const priorLocalMember = priorLocalState![i];
202+
if (priorLocalMember !== member) {
203+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
204+
if (!changed) {
205+
// console.log({
206+
// priorLocalMember,
207+
// member,
208+
// i,
209+
// priorState: priorLocalState?.slice(),
210+
// finalState: finalState.slice(),
211+
// prevState: prevState.slice(),
212+
// });
213+
}
214+
215+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-console
216+
!changed && console.log(`changed because priorLocalMember !== member && !prevSet.has(member)`);
217+
}
218+
changed = true;
219+
}
220+
}
180221
added.add(member);
181222
onAdd(member);
182223
}
@@ -207,9 +248,19 @@ function _compare<T>(
207248
if (i < priorLocalLength) {
208249
const priorLocalMember = priorLocalState![i];
209250
if (priorLocalMember !== member) {
251+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
252+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-console
253+
!changed && console.log(`changed because priorLocalMember !== member && member !== prevMember`);
254+
}
210255
changed = true;
211256
}
212-
} else {
257+
} else if (i < finalLength) {
258+
// if we have exceeded the length of priorLocalState and we are within the range
259+
// of the finalState then we must have changed
260+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
261+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-console
262+
!changed && console.log(`changed because priorMember !== member && index >= priorLocalLength`);
263+
}
213264
changed = true;
214265
}
215266
}
@@ -229,7 +280,53 @@ function _compare<T>(
229280
}
230281

231282
if (!finalSet.has(prevMember)) {
232-
changed = true;
283+
// if we are within finalLength, we can only be "changed" if we've already exceeded
284+
// the index range of priorLocalState, as otherwise the previous member may still
285+
// be removed.
286+
//
287+
// prior local: [1, 2, 3, 4]
288+
// final state: [1, 2, 3]
289+
// prev remote state: [1, 2, 5, 3, 4]
290+
// i === 2
291+
// prevMember === 5
292+
// !finalSet.has(prevMember) === true
293+
//
294+
// because we will become changed at i===3,
295+
// we do not need to worry about becoming changed at i===2
296+
// as the arrays until now are still the same
297+
//
298+
// prior local: [1, 2, 3]
299+
// final state: [1, 2, 3, 4]
300+
// prev remote state: [1, 2, 5, 3, 4]
301+
// i === 2
302+
// prevMember === 5
303+
// !finalSet.has(prevMember) === true
304+
//
305+
// because we will become changed at i===3
306+
// we do not need to worry about becoming changed at i===2
307+
//
308+
// prior local: [1, 2, 3]
309+
// final state: [1, 2, 3]
310+
// prev remote state: [1, 2, 5, 3, 4]
311+
// i === 2
312+
// prevMember === 5
313+
// !finalSet.has(prevMember) === true
314+
//
315+
// because we have same length and same membership order
316+
// we do not need to worry about becoming changed at i===2
317+
//
318+
// if you do not have a priorLocalState you can't be changed
319+
// ergo, we never need to set changed in this branch.
320+
// this log can still be useful for debugging.
321+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
322+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
323+
!changed &&
324+
// eslint-disable-next-line no-console
325+
console.log(`changed because i >= priorLocalLength && i < finalLength && !finalSet.has(prevMember)`);
326+
}
327+
//
328+
// we do still set remoteOrderChanged as it has
329+
remoteOrderChanged = true;
233330
removed.add(prevMember);
234331
onDel(prevMember);
235332
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { deprecate } from '@ember/debug';
22

3+
import { DEBUG_RELATIONSHIP_NOTIFICATIONS, LOG_METRIC_COUNTS } from '@warp-drive/build-config/debugging';
34
import {
45
DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE,
56
DISABLE_6X_DEPRECATIONS,
@@ -15,6 +16,12 @@ import { assertPolymorphicType } from '../debug/assert-polymorphic-type';
1516
import type { CollectionEdge } from '../edges/collection';
1617
import type { Graph } from '../graph';
1718

19+
function count(label: string) {
20+
// @ts-expect-error
21+
// eslint-disable-next-line
22+
globalThis.__WarpDriveMetricCountData[label] = (globalThis.__WarpDriveMetricCountData[label] || 0) + 1;
23+
}
24+
1825
/*
1926
case many:1
2027
========
@@ -90,6 +97,10 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera
9097
const wasDirty = relationship.isDirty;
9198
let localBecameDirty = false;
9299

100+
if (LOG_METRIC_COUNTS) {
101+
count(`replaceRelatedRecordsLocal ${'type' in record ? record.type : '<document>'} ${op.field}`);
102+
}
103+
93104
const onAdd = (identifier: StableRecordIdentifier) => {
94105
// Since we are diffing against the remote state, we check
95106
// if our previous local state did not contain this identifier
@@ -167,6 +178,10 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
167178
const identifiers = op.value;
168179
const relationship = graph.get(op.record, op.field);
169180

181+
if (LOG_METRIC_COUNTS) {
182+
count(`replaceRelatedRecordsRemote ${'type' in op.record ? op.record.type : '<document>'} ${op.field}`);
183+
}
184+
170185
assert(
171186
`You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`,
172187
isHasMany(relationship)
@@ -206,6 +221,14 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
206221
if (relationship.additions?.has(identifier)) {
207222
relationship.additions.delete(identifier);
208223
} else {
224+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
225+
if (!relationship.isDirty) {
226+
// eslint-disable-next-line no-console
227+
console.log(
228+
`setting relationship to dirty because the remote addition was not in our previous list of local additions`
229+
);
230+
}
231+
}
209232
relationship.isDirty = true;
210233
}
211234
addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote);
@@ -217,6 +240,14 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
217240
if (relationship.removals?.has(identifier)) {
218241
relationship.removals.delete(identifier);
219242
} else {
243+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
244+
if (!relationship.isDirty) {
245+
// eslint-disable-next-line no-console
246+
console.log(
247+
`setting relationship to dirty because the remote removal was not in our previous list of local removals`
248+
);
249+
}
250+
}
220251
relationship.isDirty = true;
221252
}
222253
removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote);

0 commit comments

Comments
 (0)