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

Pay go setup relationships #4821

Merged
merged 4 commits into from
Mar 20, 2017
Merged

Pay go setup relationships #4821

merged 4 commits into from
Mar 20, 2017

Conversation

hjdivad
Copy link
Member

@hjdivad hjdivad commented Feb 24, 2017

The goal here is to do as little work as we can with relationship payloads until the relationship is actually accessed. In an extreme case, where many relationship payloads are pushed, but no relationships are accessed, all we want to do is stash the payload.

So what we want is

  • to spend less time on relationships we don't access
  • to not spend more time (even if we spend it later on) for relationships we do access

Testing on my early 2015 13" macbook pro I get a substantial improvement on pushing relationships that aren't accessed.


For my test case of 2800 models and 4800 relationships lazy relationships is about 2.2x as fast when no relationship is accessed.

master
master-lazy

pr
pr-lazy


For my test case of 2800 models and 4800 relationships lazy relationships costs about as much as master

master
master-eager-push
master-eager-expand

pr
pr-eager-push
pr-eager-expand

@hjdivad hjdivad force-pushed the pay-go-setup-relationships branch 2 times, most recently from ac26ee6 to c6a496e Compare March 2, 2017 22:05
Copy link
Member

@stefanpenner stefanpenner left a comment

Choose a reason for hiding this comment

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

I've done a partial review, will do more next.

*/
_getRelationshipPayloads(modelName, relationshipName) {
// TODO: lightschema this
let modelClass = this._store._modelFor(modelName);
Copy link
Member

Choose a reason for hiding this comment

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

should this be _modelFor over store.modelFor?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's strictly internal, so by the time we get here modelName should already be normalized. I'm fine with normalizing here (ie calling this._store.modelFor instead of this._store._modelFor) for simplicity's sake.

export default class RelationshipsPayloads {
constructor(store) {
this._store = store;
this._map = {};
Copy link
Member

Choose a reason for hiding this comment

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

thoughts on renaming this to cache with a comment of // cache for relationshipPayloads?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

*/
_getRelationshipPayloads(modelName, relationshipName) {
// TODO: lightschema this
let modelClass = this._store._modelFor(modelName);
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if _getRelationshipPayload should take also take relationshipsByName as an argument.

In push and unload the callers of this method invoke it in a loop and for each iteration of the loop modelClass is constant, but yet each iteration must repeatedly lookup modelClass modelClass.relationshipsByName.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@@ -463,8 +463,8 @@ testInDebug("Inverse relationships that don't exist throw a nice error for a bel
}, /We found no inverse relationships by the name of 'testPost' on the 'user' model/);
});

test("inverseFor is only called when inverse is not null", function(assert) {
assert.expect(2);
test("inverseFor short-circuits when inverse is null", function(assert) {
Copy link
Member

Choose a reason for hiding this comment

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

Now that we have both inverseFor and _inverseFor should we test both of them?

Copy link
Member Author

Choose a reason for hiding this comment

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

Looking more closely, we don't need to introduce _inverseFor; the point was just to separate the API from the work so we can test that the expensive path is not taken, but this separation already exists between inverseFor and _findInverseFor, so I've updated the tests to assert that inverseFor short-circuits _findInverseFor rather than _inverseFor.

@@ -505,6 +505,7 @@ export default class InternalModel {
assert("Cannot destroy an internalModel while its record is materialized", !this.record || this.record.get('isDestroyed') || this.record.get('isDestroying'));

this.store._removeFromIdMap(this);
this.store._relationshipsPayloads.unload(this.modelName, this.id);
Copy link
Member

Choose a reason for hiding this comment

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

should we unify 507 and 508 into this.store._internalModelDestroyed(internalModel) that way, we don't need to leak _removeFromIdMap and _relationshipsPayload private members to internalModel

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@@ -988,6 +991,8 @@ export default class InternalModel {
*/
adapterDidCommit(data) {
if (data) {
this.store._relationshipsPayloads.push(this.modelName, this.id, data.relationships);
Copy link
Member

Choose a reason for hiding this comment

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

}
}

@class RelationshipsPayloads
Copy link
Member

Choose a reason for hiding this comment

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

likely should be marked as @private

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

*/
_inverseFor(modelName, relationshipName) {
// TODO: lightschema this
let modelClass = this._store._modelFor(modelName);
Copy link
Member

Choose a reason for hiding this comment

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

should this by modelFor?

Copy link
Member Author

Choose a reason for hiding this comment

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

}
}

_hasRecordForId(store, type, id) {
Copy link
Member

Choose a reason for hiding this comment

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

this method does not appear to rely on this, we should likely move it off the prototype and just to a module local helper function. Or, we should make it use this._store internally.

When I see methods on this I feel it suggests it needs access to this.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

get _lhsRelationshipIsMany() {
if (this.__lhsRelatinoshipIsMany === undefined) {
if (this._lhsModelName) {
let modelClass = this._store.modelFor(this._lhsModelName);
Copy link
Member

Choose a reason for hiding this comment

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

I believe both LHS and RHS modelClass's are loaded when RelationshipPayload is instantiated, should we just pass them in at construction? At that point, do we already have the relationship descriptor as well, or is that what we want lazy?

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe both LHS and RHS modelClass's are loaded when RelationshipPayload is instantiated, should we just pass them in at construction? At that point, do we already have the relationship descriptor as well, or is that what we want lazy?

This is not necessarily the case. inverseFor does not load the inverse class. Even when we check for the inverse relationship, we won't cause the inverse class to be loaded.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not necessarily the case. inverseFor does not load the inverse class. Even when we check for the inverse relationship, we won't cause the inverse class to be loaded.

Okay what I said is just wrong. I check this but obviously did not do so correctly. I'll push the classes down to RelationshipPayloads

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm just passing in the relationship meta since all we want off of it is to know whether or not this side of the relationship is a many side.

@hjdivad hjdivad force-pushed the pay-go-setup-relationships branch 3 times, most recently from cb16735 to c53997e Compare March 10, 2017 00:10
@hjdivad
Copy link
Member Author

hjdivad commented Mar 10, 2017

@stefanpenner i've responded to your review + done some additional perf tweaks. I've kept the changes separate, in c53997e if you want to look at only the changes

return this._inverseFor(name, store);
},

_inverseFor(name, store) {
let inverseMap = get(this, 'inverseMap');
if (inverseMap[name]) {
Copy link
Member

Choose a reason for hiding this comment

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

should this also check for null and if it is null return null, rather then failing this branch and looking it up again on 1315 only to find null again?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

if (rel) {
relationship = relationships[key] = createRelationshipFor(internalModel, rel, internalModel.store);
}
if (!rel) { return undefined; }
Copy link
Member

Choose a reason for hiding this comment

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

should we move this return to before line 60, that way we don't bother getting the relationshipPayload at all?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

relationship.pushFromInverse(payload);
}
if (relationshipPayload) {
this.internalModel.store._backburner.join(function() {
Copy link
Member

Choose a reason for hiding this comment

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

=>

Copy link
Member

Choose a reason for hiding this comment

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

i'm surprised this join is required, whats it all about?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is the thing that batches canonical flushes. In master this batching happens within _push

if (this.inverseKey) {
let relationships = internalModel._relationships;
let relationshipExisted = relationships.has(this.inverseKey);
let relationship = relationships.get(this.inverseKey);
Copy link
Member

Choose a reason for hiding this comment

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

should we move this line to 72, that way if the branch is not taken we don't lazily create the relationship at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

No; this PR preserves the invariant that if a relationship is initialized, its inverse is also initialized.

We could change the invariant, but it would add complexity in the case where a relationship A gets its initial state from the payload of its inverse B, B is initialized and then mutated before A is initialized.

This is something we could do but it seems better to leave that for a separate PR

@@ -2839,6 +2839,7 @@ test("deleted records should stay deleted", function(assert) {
message.destroyRecord()
});

self.halt=true;
Copy link
Member

Choose a reason for hiding this comment

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

^

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@@ -2853,7 +2854,12 @@ test("deleted records should stay deleted", function(assert) {
}
}]
});
self.halt=false;
Copy link
Member

Choose a reason for hiding this comment

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

^

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@@ -518,6 +518,8 @@ export default class InternalModel {

setupData(data) {
heimdall.increment(setupData);
this.store._internalModelHasRelationshipData(this.modelName, this.id, data.relationships);
Copy link
Member

Choose a reason for hiding this comment

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

This sounds like a boolean returning method, maybe something like received or process or something would be better?

Copy link
Member Author

Choose a reason for hiding this comment

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

_internalModelDidGetGetRelationshipData?

return inverse;
let relationship = get(this, 'relationshipsByName').get(name);
if (!relationship) {
return inverseMap[name] = null;
Copy link
Member

Choose a reason for hiding this comment

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

Can we not assign & return at the same time

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

}

let options = relationship.options;
if (options && options.inverse === null) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment here please

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

let get = Ember.get;

/**
Manages relationship payloads for a given store. Acts as a single source of
Copy link
Member

Choose a reason for hiding this comment

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

Can you expand on this comment to say that this is only until the relationship is initialized

get(modelName, id, relationshipName) {
let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, false);
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment as to why this signature is so long

Copy link
Member Author

Choose a reason for hiding this comment

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

👍


let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
for (let key in relationshipsData) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this safe to do?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah yeah, i'll switch this to Object.keys().forEach

let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
for (let key in relationshipsData) {
let relationshipPayloads = this._getRelationshipPayloads(modelName, key, modelClass, relationshipsByName, true);
Copy link
Member

Choose a reason for hiding this comment

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

Maybe passing the relationship would simplify things?

Copy link
Member Author

Choose a reason for hiding this comment

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

unfortunately we'd need to also pass the inverse relationship, which we don't always need to compute

return this._cache[key];
}

_initializeRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName) {
Copy link
Member

Choose a reason for hiding this comment

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

This method could use comments or breaking apart

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

this._rhsRelationshipMeta = inverseRelationshipMeta;

this._lhsPayloads = {};
if (modelName !== inverseModelName || relationshipName !== inverseRelationshipName) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain the goal behind this conditional

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added some comments in the then clause to explain, as well as expanded the comments in the else clause.

 // then clause

      // The common case of a non-reflexive relationship, or a reflexive
      // relationship whose inverse is not itself

// else clause

      // Edge case when we have a reflexive relationship to itself
      //  eg user hasMany friends inverse friends
      //
      //  In this case there aren't really two sides to the relationship, but
      //  we set `_rhsPayloads = _lhsPayloads` to make things easier to reason
      //  about

this._isReflexive = true;
}

// either canoical on push or pending & flush
Copy link
Member

Choose a reason for hiding this comment

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

This comment doesn't really tell me very much

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

get(modelName, id, relationshipName) {
this._flushPending();

if (modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment explaining the logic behind mapping to lhs or rhs

Copy link
Member Author

Choose a reason for hiding this comment

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

👍


if (modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName) {
return this._lhsPayloads[id];
} else {
Copy link
Member

Choose a reason for hiding this comment

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

let's add a debug check that asserts this is actually === rhsRelationshipName

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

unload(modelName, id, relationshipName) {
this._flushPending();

if (modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe extract into a method?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

_flushPending() {
if (this._pendingPayloads.length === 0) { return; }

let work = this._pendingPayloads.splice(0, this._pendingPayloads.length);
Copy link
Member

Choose a reason for hiding this comment

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

can we rename this to payloadsToBeProcessed or something like that

Copy link
Member Author

Choose a reason for hiding this comment

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

sure

}
}

if (modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment explaining the intent behind this blob

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

}
}

_inverseLoaded(payload) {
Copy link
Member

Choose a reason for hiding this comment

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

Seems bad that the invariant from unloading where graphs are either unloaded together or not at all, is changed here

}
}

_populateInverse(relationshipData, inversePayload, inversePayloads, inverseIsMany) {
Copy link
Member

Choose a reason for hiding this comment

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

When we rename the classes, can we rename this arguments as well please

Copy link
Member Author

Choose a reason for hiding this comment

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

k i've renamed inversePayloads to inverseIdToPayloads here and elsewhere

@hjdivad hjdivad force-pushed the pay-go-setup-relationships branch 2 times, most recently from e42e839 to dd55489 Compare March 14, 2017 20:08
}
}

function hasRecordForId(store, type, id) {
Copy link
Member

Choose a reason for hiding this comment

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

In case this sticks around can we verify this is needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

it's gone

@@ -36,6 +36,18 @@ export default class BelongsToRelationship extends Relationship {
this.flushCanonicalLater();
}

setInitialCanonicalRecord(record) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this to a separate PR

Copy link
Member Author

Choose a reason for hiding this comment

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

ok

Copy link
Member Author

Choose a reason for hiding this comment

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

moved to a separate commit within this PR in case we want to revert separately

relationship = relationships[key] = createRelationshipFor(internalModel, rel, internalModel.store);

if (relationshipPayload) {
this.internalModel.store._backburner.join(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Check if this is needed

}
this.flushCanonicalLater();
this.setHasData(true);
}

setupInverseRelationship(internalModel, initial) {
Copy link
Member

Choose a reason for hiding this comment

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

We don't need initial here

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

let relationships = internalModel._relationships;
let relationshipExisted = relationships.has(this.inverseKey);
let relationship = relationships.get(this.inverseKey);
if (relationshipExisted || this.isPolymorphic) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave comments explaining

  1. The laziness
  2. why we are not lazy in the polymorphic case

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

} else {
let relationships = internalModel._implicitRelationships;
let relationship = relationships[this.inverseKeyForImplicit];
if (!relationship) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here

Copy link
Member Author

Choose a reason for hiding this comment

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

The semantics here have not changed at all:

} else {
if (!record._implicitRelationships[this.inverseKeyForImplicit]) {
record._implicitRelationships[this.inverseKeyForImplicit] = new Relationship(this.store, record, this.key, { options: {} });
}
record._implicitRelationships[this.inverseKeyForImplicit].addCanonicalRecord(this.record);
}

@@ -2403,14 +2412,15 @@ Store = Service.extend({
heimdall.increment(_setupRelationships);
let setupToken = heimdall.start('store._setupRelationships');
let pushed = this._pushedInternalModels;
let modelNameToInverseMap = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

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

Lets see if there are perf benefits to caching this across runs

@@ -2403,14 +2412,15 @@ Store = Service.extend({
heimdall.increment(_setupRelationships);
let setupToken = heimdall.start('store._setupRelationships');
let pushed = this._pushedInternalModels;
let modelNameToInverseMap = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment as to why we do this

@@ -2787,19 +2797,82 @@ function _commit(adapter, store, operation, snapshot) {
}, label);
}

function setupRelationships(store, internalModel, data) {
function inverseRelationshipInitialized(store, internalModel, data, key, modelNameToInverseMap) {
Copy link
Member

Choose a reason for hiding this comment

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

lets rename this to isInverseRelationshipInitialized

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

inverseMap = modelNameToInverseMap[internalModel.modelName] = get(internalModel.type, 'inverseMap');
}
let inverseData = inverseMap[key];
if (inverseData === undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

lets rename this to something like inverseRelationshipMetadata/something not containing the word Data

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

let relationship = internalModel._relationships.get(key);
relationship.push(data.relationships[key]);
});
for (let key in data.relationships) {
Copy link
Member

Choose a reason for hiding this comment

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

lets try switching this back to eachRelationship

Copy link
Member Author

Choose a reason for hiding this comment

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

yep this seems good; also with the setInitial work the backburner.join run in relationships/store/create seems unnecessary

@hjdivad hjdivad force-pushed the pay-go-setup-relationships branch 3 times, most recently from bf5db27 to 97dd39d Compare March 16, 2017 17:34
@hjdivad
Copy link
Member Author

hjdivad commented Mar 16, 2017

@igorT @stefanpenner this is ready to merge.

@stefanpenner
Copy link
Member

:shipit:

if (relationshipRequiresNotification) {
let relationshipData = data.relationships[relationshipName];
relationships.get(relationshipName).push(relationshipData);
} else {
Copy link
Member

Choose a reason for hiding this comment

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

We were gonna remove this else right?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah you're right, LMK push this fix

hjdivad added 4 commits March 20, 2017 13:53
- add option for expanding relationships
- make eager materialize materialize included as well
The goal here is to do as little work as we can with relationship payloads until
the relationship is actually accessed.  In an extreme case, where many
relationship payloads are pushed, but no relationships are accessed, all we want
to do is stash the payload.
We don't need to go through `computeChanges`, `flushCqnonical` &c. when
initializing relationships.  Treating initial payloads the same as
updated payloads is costly.
@hjdivad hjdivad force-pushed the pay-go-setup-relationships branch from 97dd39d to f8304b2 Compare March 20, 2017 20:53
@hjdivad hjdivad merged commit 552e1d6 into master Mar 20, 2017
@hjdivad hjdivad deleted the pay-go-setup-relationships branch March 20, 2017 22:51
@rlivsey
Copy link

rlivsey commented Apr 25, 2017

Looks like this has broken for JSONAPI payloads which contain links for hasMany associations.

When accessing a relationship in a template I now get the following error, going to before this PR was merged works fine:

Assertion Failed: You modified "meeting.attendees" twice on <minutebase@model:meeting::ember1351:772c5e6e-aceb-11e6-ae79-770161ca087d> in a single render. It was rendered in "component:mb-pages/dashboard/meeting" and modified in "component:mb-pages/dashboard/meeting". This was unreliable and slow in Ember 1.x and is no longer supported. See https://github.com/emberjs/ember.js/issues/13948 for more details.
Error
    at assert (http://minutebase.dev:4202/assets/vendor.js:21770:13)
    at assert (http://minutebase.dev:4202/assets/vendor.js:33521:34)
    at Object.assertNotRendered (http://minutebase.dev:4202/assets/vendor.js:39296:11)
    at Object.propertyDidChange (http://minutebase.dev:4202/assets/vendor.js:37912:30)
    at Class.propertyDidChange (http://minutebase.dev:4202/assets/vendor.js:51064:19)
    at Class.notifyPropertyChange (http://minutebase.dev:4202/assets/vendor.js:51078:12)
    at InternalModel.notifyPropertyChange (http://minutebase.dev:4202/assets/vendor.js:140423:21)
    at ManyRelationship.updateLink (http://minutebase.dev:4202/assets/vendor.js:147451:19)
    at ManyRelationship.push (http://minutebase.dev:4202/assets/vendor.js:147529:16)
    at Relationships.get (http://minutebase.dev:4202/assets/vendor.js:146885:24)

Looks like updateLink is notifying property change and causing Glimmer to re-evaluate the relationship and so it explodes.

Tried to make a Twiddle, but the example Twiddle appears to be broken at the moment.

@stefanpenner
Copy link
Member

stefanpenner commented Apr 25, 2017

@rlivsey can you open a new issue so this isn't lost? (also ideally a failing test suite will make this much more actionable)

@rlivsey
Copy link

rlivsey commented Apr 25, 2017

@stefanpenner yup, was trying to make a repro first but will open an issue once I get the kids to bed!

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.

5 participants