Skip to content
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

Closed
wants to merge 16 commits into from
Closed

[PERF] optimise notifications when has-many array is changed #4583

wants to merge 16 commits into from

Conversation

BryanCrotaz
Copy link
Contributor

diff the old and new array and optimise the notifications accordingly.

Particularly important when polling server for changes with a repeating model.reload() or relationship.update()

@BryanCrotaz
Copy link
Contributor Author

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.

@stefanpenner
Copy link
Member

stefanpenner commented Oct 18, 2016

Cool, i was hoping someone would work on this! I will try to find some time to review this.

@BryanCrotaz
Copy link
Contributor Author

Whaddayathink?

@BryanCrotaz BryanCrotaz changed the title optimise notifications when has-many array is changed [PERF] optimise notifications when has-many array is changed Oct 21, 2016
.gitignore Outdated
@@ -24,3 +24,4 @@ node_modules/
bower_components/
.metadata_never_index
npm-debug.log
.vscodeignore
Copy link
Member

@stefanpenner stefanpenner Oct 21, 2016

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.

Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const { run } = Ember

this.set('length', toSet.length);
}
this.currentState = toSet;
this.arrayContentDidChange(firstChangeIndex, added, removed);
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor Author

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

var firstChangeIndex = -1; // -1 signifies no changes
// find the first change
var currentArray = this.currentState;
for (var i=0; i<shortestLength; i++) {
Copy link
Member

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.

Copy link
Contributor Author

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

@@ -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)) {
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
if (firstChangeIndex !== -1) {
// we found a change
var added = newLength - firstChangeIndex;
Copy link
Member

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]

Copy link
Contributor Author

@BryanCrotaz BryanCrotaz Oct 21, 2016

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...

Copy link
Member

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]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimised in latest commit

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)) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@stefanpenner
Copy link
Member

cc @hjdivad your thoughts on diffing/batching would be handy here.

@BryanCrotaz
Copy link
Contributor Author

@stefanpenner Implemented a better change finder, but now the internals of arrayContentWillChange are failing and I can't see how - seems like the target of the call is the array, not the observer. Have a feeling I'll cause more damage than I fix if I try and work on this one.

@runspired
Copy link
Contributor

@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?

@BryanCrotaz
Copy link
Contributor Author

@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

@BryanCrotaz
Copy link
Contributor Author

@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?

@runspired
Copy link
Contributor

@BryanCrotaz morning my time is good :)

@BryanCrotaz
Copy link
Contributor Author

Invite me to a hangout when you're ready

@runspired
Copy link
Contributor

I ended up working on this problem last night by accident :D

@BryanCrotaz
Copy link
Contributor Author

@runspired :) Did you get anywhere? Still happy to pair...

Conflicts:
	addon/-private/system/many-array.js
	tests/integration/records/relationship-changes-test.js
@BryanCrotaz
Copy link
Contributor Author

Odd - many-array flushCanonical is not being called any more during store.push so none of my optimisations are kicking in.

What's the new pattern?

}
const added = newLength - unchangedEndBlockLength - firstChangeIndex;
const removed = oldLength - unchangedEndBlockLength - firstChangeIndex;
this.arrayContentWillChange(firstChangeIndex, removed, added);
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// 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;
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@stefanpenner
Copy link
Member

  • address feedback
  • tests green
  • rebase

@BryanCrotaz
Copy link
Contributor Author

@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?

@BryanCrotaz
Copy link
Contributor Author

Very strange.

I have a test that pushes a siblings hasMany into a person using store.push.

This observer fires ok

    person.hasMany('siblings').hasManyRelationship.getManyArray().addObserver('[]', function() {
      observerCount++;
    });

This observer does not

    person.addObserver('siblings.[]', function() {
      observerCount++;
    });

@sly7-7
Copy link
Contributor

sly7-7 commented Mar 8, 2017

@BryanCrotaz Maybe because siblings.[] is not really the same as the manyarray ?

@BryanCrotaz
Copy link
Contributor Author

@sly7-7 This test passed back in October. If I'm observing siblings.[] on a person I'd expect that observer to trigger when the relationship is changed via a store.push.

@sly7-7
Copy link
Contributor

sly7-7 commented Mar 8, 2017

@BryanCrotaz I fully agree with that. Are the tests failing because of your work ? Or do they not pass with the actual code ?

@BryanCrotaz
Copy link
Contributor Author

I think they're not passing because of changes on master

@@ -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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isInitialized no longer needed?

this.relationship.notifyHasManyChanged();
}
this.record.updateRecordArrays();
Copy link
Member

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?

Copy link
Contributor Author

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


if (diff.firstChangeIndex !== null) { // it's null if no change found
// we found a change
this.arrayContentWillChange(diff.firstChangeIndex || 0, diff.removedCount, diff.addedCount);
Copy link
Member

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 ?

Copy link
Contributor Author

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

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

@@ -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);
Copy link
Member

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();
Copy link
Member

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();
Copy link
Member

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();
Copy link
Member

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();
Copy link
Member

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();
Copy link
Member

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.

@stefanpenner
Copy link
Member

@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.

@BryanCrotaz
Copy link
Contributor Author

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.

@stefanpenner
Copy link
Member

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.

@BryanCrotaz
Copy link
Contributor Author

@stefanpenner I think all your points are dealt with

@stefanpenner
Copy link
Member

@BryanCrotaz very cool, will review thoroughly in the AM \w @igorT

@BryanCrotaz
Copy link
Contributor Author

Unfortunately... it's so efficient that CPs hung off a peekAll don't work any more!

@BryanCrotaz
Copy link
Contributor Author

@stefanpenner I need some help on this one.

I've added a test for the following code not working.

test('Calling push with relationship triggers observer of array if the relationship was empty and is added to', function(assert) {
// ...
    person.addObserver('siblings.[]', function() {
      observerCount++;
    });
// ...

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?

@stefanpenner
Copy link
Member

@BryanCrotaz let me take a look

@stefanpenner
Copy link
Member

@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) {

@BryanCrotaz
Copy link
Contributor Author

BryanCrotaz commented Mar 9, 2017

@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

@BryanCrotaz
Copy link
Contributor Author

I think this PR is complete now.

@BryanCrotaz
Copy link
Contributor Author

The diff is a bit of a mess so I'll resubmit as a new PR with it all flattened.

@BryanCrotaz
Copy link
Contributor Author

replaced by PR #4850

@BryanCrotaz BryanCrotaz closed this Mar 9, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants