Skip to content

Commit e56f697

Browse files
gitKrystanrunspired
authored andcommitted
Don't overnotify for updates to added state that match local updates (emberjs#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 606213b commit e56f697

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,
@@ -52,7 +53,13 @@ function _deprecatedCompare<T>(
5253
adv = true;
5354

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

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

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

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

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 { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations';
45
import { DEBUG } from '@warp-drive/build-config/env';
56
import { assert } from '@warp-drive/build-config/macros';
@@ -12,6 +13,12 @@ import { assertPolymorphicType } from '../debug/assert-polymorphic-type';
1213
import type { CollectionEdge } from '../edges/collection';
1314
import type { Graph } from '../graph';
1415

16+
function count(label: string) {
17+
// @ts-expect-error
18+
// eslint-disable-next-line
19+
globalThis.__WarpDriveMetricCountData[label] = (globalThis.__WarpDriveMetricCountData[label] || 0) + 1;
20+
}
21+
1522
/*
1623
case many:1
1724
========
@@ -87,6 +94,10 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera
8794
const wasDirty = relationship.isDirty;
8895
let localBecameDirty = false;
8996

97+
if (LOG_METRIC_COUNTS) {
98+
count(`replaceRelatedRecordsLocal ${'type' in record ? record.type : '<document>'} ${op.field}`);
99+
}
100+
90101
const onAdd = (identifier: StableRecordIdentifier) => {
91102
// Since we are diffing against the remote state, we check
92103
// if our previous local state did not contain this identifier
@@ -164,6 +175,10 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
164175
const identifiers = op.value;
165176
const relationship = graph.get(op.record, op.field);
166177

178+
if (LOG_METRIC_COUNTS) {
179+
count(`replaceRelatedRecordsRemote ${'type' in op.record ? op.record.type : '<document>'} ${op.field}`);
180+
}
181+
167182
assert(
168183
`You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`,
169184
isHasMany(relationship)
@@ -203,6 +218,14 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
203218
if (relationship.additions?.has(identifier)) {
204219
relationship.additions.delete(identifier);
205220
} else {
221+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
222+
if (!relationship.isDirty) {
223+
// eslint-disable-next-line no-console
224+
console.log(
225+
`setting relationship to dirty because the remote addition was not in our previous list of local additions`
226+
);
227+
}
228+
}
206229
relationship.isDirty = true;
207230
}
208231
addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote);
@@ -214,6 +237,14 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper
214237
if (relationship.removals?.has(identifier)) {
215238
relationship.removals.delete(identifier);
216239
} else {
240+
if (DEBUG_RELATIONSHIP_NOTIFICATIONS) {
241+
if (!relationship.isDirty) {
242+
// eslint-disable-next-line no-console
243+
console.log(
244+
`setting relationship to dirty because the remote removal was not in our previous list of local removals`
245+
);
246+
}
247+
}
217248
relationship.isDirty = true;
218249
}
219250
removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote);

0 commit comments

Comments
 (0)