-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[PERF] optimise notifications when has-many array is changed #4583
Conversation
tested in our app where we refresh a large relationship every second and have 10 or so filters chained off this. CPU usage has gone from 100% of one core to a trickle. |
Cool, i was hoping someone would work on this! I will try to find some time to review this. |
Whaddayathink? |
.gitignore
Outdated
@@ -24,3 +24,4 @@ node_modules/ | |||
bower_components/ | |||
.metadata_never_index | |||
npm-debug.log | |||
.vscodeignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsure if this should be added here, typically editor specific things are not included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove
import DS from 'ember-data'; | ||
|
||
var env, store; | ||
const attr = DS.attr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const { attr, hasMany} = DS
|
||
import DS from 'ember-data'; | ||
|
||
var env, store; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let
var env, store; | ||
const attr = DS.attr; | ||
const hasMany = DS.hasMany; | ||
const run = Ember.run; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const { run } = Ember
addon/-private/system/many-array.js
Outdated
this.set('length', toSet.length); | ||
} | ||
this.currentState = toSet; | ||
this.arrayContentDidChange(firstChangeIndex, added, removed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as we are notifiying array change events, tests should likely test that these are accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but found a bug in array change event mechanism
addon/-private/system/many-array.js
Outdated
var firstChangeIndex = -1; // -1 signifies no changes | ||
// find the first change | ||
var currentArray = this.currentState; | ||
for (var i=0; i<shortestLength; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should likely skip this if within init
, but that isn't needed i think until #4600 is merged, so either this one or that one need to take this into account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what you mean
addon/-private/system/many-array.js
Outdated
@@ -87,15 +87,39 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { | |||
); | |||
toSet = toSet.concat(newRecords); | |||
var oldLength = this.length; | |||
this.arrayContentWillChange(0, this.length, toSet.length); | |||
// It’s possible the parent side of the relationship may have been unloaded by this point | |||
if (_objectIsAlive(this)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the object is dead, we should exit at the top of the function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
addon/-private/system/many-array.js
Outdated
} | ||
if (firstChangeIndex !== -1) { | ||
// we found a change | ||
var added = newLength - firstChangeIndex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if we have non-contiguous changes, e.g. the collection was replaced and some members of the previous set and the current set are the same.
[model#1, model#2, model#3, model#4, model#10]
turns into:
[model#1, model#5, model#10]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we'd put out
firstChange: 1
removed: 4
added: 2
... currently
Not sure what the spec is on removed and added - would it be more correct to output
firstChange: 1
removed: 2
added: 1
... could count back from end to find the last change...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the following example may be clearer as to what the problem may be.
[model#1, model#2, model#3, model#4, model#10]
becomes:
[model#1, model#10, model#3, model#11, model#10]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optimised in latest commit
addon/-private/system/many-array.js
Outdated
var removed = oldLength - firstChangeIndex; | ||
this.arrayContentWillChange(firstChangeIndex, added, removed); | ||
// It’s possible the parent side of the relationship may have been unloaded by this point | ||
if (_objectIsAlive(this)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should have already exited above if the object is dead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
cc @hjdivad your thoughts on diffing/batching would be handy here. |
@stefanpenner Implemented a better change finder, but now the internals of |
@BryanCrotaz I'd love to land this, think you'd have a little time to pair / cleanup either this Sunday or the week after the Thanksgiving break? |
@runspired as I said in my comment above I think I need some help - the latest problem is unrelated to what I've been doing and I don't have any knowledge |
@runspired sorry just reread your comment and realised what you're suggesting yes Sunday would be fine. I'm on GMT so morning your time? |
@BryanCrotaz morning my time is good :) |
Invite me to a hangout when you're ready |
I ended up working on this problem last night by accident :D |
@runspired :) Did you get anywhere? Still happy to pair... |
Conflicts: addon/-private/system/many-array.js tests/integration/records/relationship-changes-test.js
Odd - many-array What's the new pattern? |
addon/-private/system/many-array.js
Outdated
} | ||
const added = newLength - unchangedEndBlockLength - firstChangeIndex; | ||
const removed = oldLength - unchangedEndBlockLength - firstChangeIndex; | ||
this.arrayContentWillChange(firstChangeIndex, removed, added); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if nothing changed, should be bail out here entirely? Seems like we don't want to emit any events in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
addon/-private/system/many-array.js
Outdated
// It’s possible the parent side of the relationship may have been unloaded by this point | ||
if (_objectIsAlive(this)) { | ||
this.set('length', toSet.length); | ||
const oldLength = this.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what follows is relatively complicated, could it be extracted to its own "pure" function so it can be thoroughly unit tested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
@stefanpenner rest of tests are failing because of a change on master. relationship.push(data) used to call manyArray.flushCanonical but doesn't any more. I'm not sure why this would be the case. What's the correct behaviour here? |
Very strange. I have a test that pushes a This observer fires ok
This observer does not
|
@BryanCrotaz Maybe because |
@sly7-7 This test passed back in October. If I'm observing |
@BryanCrotaz I fully agree with that. Are the tests failing because of your work ? Or do they not pass with the actual code ? |
I think they're not passing because of changes on master |
addon/-private/system/many-array.js
Outdated
@@ -141,31 +150,36 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { | |||
return this.currentState[index].getRecord(); | |||
}, | |||
|
|||
flushCanonical(isInitialized = true) { | |||
let toSet = this.canonicalState; | |||
flushCanonical() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isInitialized
no longer needed?
addon/-private/system/many-array.js
Outdated
this.relationship.notifyHasManyChanged(); | ||
} | ||
this.record.updateRecordArrays(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there is no change found, why invoke updateRecordArrays
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that's where the canonical was updated. I'll go and check again
addon/-private/system/many-array.js
Outdated
|
||
if (diff.firstChangeIndex !== null) { // it's null if no change found | ||
// we found a change | ||
this.arrayContentWillChange(diff.firstChangeIndex || 0, diff.removedCount, diff.addedCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when is diff.firstChangeIndex
falsey and not 0 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah - that's left over from some testing
addon/-private/system/many-array.js
Outdated
this.arrayContentWillChange(diff.firstChangeIndex || 0, diff.removedCount, diff.addedCount); | ||
set(this, 'length', toSet.length); | ||
this.currentState = toSet; | ||
this.arrayContentDidChange(diff.firstChangeIndex || 0, diff.removedCount, diff.addedCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^
addon/-private/system/many-array.js
Outdated
@@ -174,14 +188,15 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { | |||
} | |||
this.arrayContentWillChange(idx, amt, objects.length); | |||
this.currentState.splice.apply(this.currentState, [idx, amt].concat(objects)); | |||
this.set('length', this.currentState.length); | |||
set(this, 'length', this.currentState.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change seems unneeded
run(function() { | ||
assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); | ||
assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test does not actually appear async, the done
can be removed.
run(function() { | ||
assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); | ||
assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test does not actually appear async, the done
can be removed.
run(function() { | ||
assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); | ||
assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test does not actually appear async, the done
can be removed.
run(function() { | ||
assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); | ||
assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test does not actually appear async, the done
can be removed.
run(function() { | ||
assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); | ||
assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test does not actually appear async, the done
can be removed.
@BryanCrotaz I played with this approach in your example app, it very much improved performance of the pathological cases. I believe it is very much worth exploring further. I am spending some time with @igorT tomorrow, we will be talking about this and how to move it forward. I have left some feedback, that being addressed will help move this forward. |
We have lists with 100 entries where we're filtering on a model attribute. We now only check new array entries and with these changes we've see 50-fold changes in cpu usage. |
Yup, the downstream side-affects of overchatty arrays can be massive. |
@stefanpenner I think all your points are dealt with |
@BryanCrotaz very cool, will review thoroughly in the AM \w @igorT |
Unfortunately... it's so efficient that CPs hung off a |
@stefanpenner I need some help on this one. I've added a test for the following code not working.
The observer doesn't trigger. However the array observer on the many array in other tests does trigger. How does an observer of 'array.[]' get triggered? EDIT: Odd - this test doesn't pass on master either. Can't see anything wrong with it though. Thoughts? |
@BryanCrotaz let me take a look |
@BryanCrotaz it appears you CP is never accessed, so the observer has nothing to watch. The following addresses the issue, but the test fails because relationships do silly things still and we mutate the array twice not once (we will have to address in the future) commit 7b8ad1a47bd2ae8e999b95e26b15a4cecffa378f
Author: Stefan Penner <[email protected]>
Date: Wed Mar 8 20:12:51 2017 -0800
ensure the CP is accessed at-least once, so that the observer has something to observe
diff --git a/tests/integration/records/relationship-changes-test.js b/tests/integration/records/relationship-changes-test.js
index b2595315..ed99e5a5 100644
--- a/tests/integration/records/relationship-changes-test.js
+++ b/tests/integration/records/relationship-changes-test.js
@@ -276,7 +276,6 @@ test('Calling push with relationship recalculates computed alias property to fir
test('Calling push with relationship triggers observer of array if the relationship was empty and is added to', function(assert) {
assert.expect(1);
- let person = null;
let observerCount = 0;
run(() => {
@@ -297,12 +296,15 @@ test('Calling push with relationship triggers observer of array if the relations
});
});
- person = store.peekRecord('person', 'wat');
+ let person = store.peekRecord('person', 'wat');
run(() => {
- person.addObserver('siblings.[]', function() {
- observerCount++;
- });
+ // materialize the CP, so that the later siblings observer enages (observers
+ // dont have side affects, so they cause a cp, in this case a relationship, to
+ // be accessed themsleves).
+ person.get('siblings');
+
+ person.addObserver('siblings.[]', () => observerCount++);
});
run(() => {
@@ -324,9 +326,7 @@ test('Calling push with relationship triggers observer of array if the relations
});
});
- run(() => {
- assert.equal(observerCount, 1, 'siblings observer should be triggered once');
- });
+ assert.equal(observerCount, 1, 'siblings observer should be triggered once');
});
test('Calling push with relationship triggers observers once if the relationship was not empty and was added to', function(assert) { |
@stefanpenner doh! that old chestnut. I've found another inefficiency that I don't know whether to fix. manyArray fires property change events during destruction. Does it need to? Is that going to break something else? EDIT: appears it's needed so that CP caches are cleared when relationships are unloaded |
I think this PR is complete now. |
The diff is a bit of a mess so I'll resubmit as a new PR with it all flattened. |
replaced by PR #4850 |
diff the old and new array and optimise the notifications accordingly.
Particularly important when polling server for changes with a repeating
model.reload()
orrelationship.update()