-
-
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
[Feat] simplified relationship state #4882
[Feat] simplified relationship state #4882
Conversation
export default class UniqueArray { | ||
constructor(key) { | ||
this.key = key; | ||
this.seen = Object.create(null); |
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.
consider not initializing seen until array is > say 20 items.
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.
👍
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 seems awfully similar to Ember.OrderedSet
We should just tweak that, rather then adding something similar. Note: Ember basically provides OrderedSet
and friends only for ember-data.
I believe we can improve the performance of (small N) ordered set by taking the approach outlined above by @krisselden, it performs quite well for large N.
We should also expose list
as a public attribute on it.
array.push(relationship.currentState, relationship.canonicalState); | ||
return; | ||
case 'has-many': | ||
additions = [].concat(relationship.currentState, relationship.canonicalState); |
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.
do 2 pushes instead of concat
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 was debating this, any particular reason why?
destroy() { | ||
if (!this.inverseKey) { return; } | ||
|
||
let allMembers = |
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.
can we not copy to an array just to iterate over?
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.
Seeing as this doesn't unique the sets either it's doubly inefficient.
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.
(also fwiw this is the legacy path that everything used to do, now only implicit does it, I haven't spent much time optimizing implicit's code yet)
|
||
addInternalModels(internalModels, idx) { | ||
heimdall.increment(addInternalModels); | ||
internalModels.forEach(internalModel => { |
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 this is a hot path consider avoiding forEach
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/snapshot.js
Outdated
|
||
if (hasData) { | ||
results = []; | ||
members.forEach((member) => { | ||
currentState.forEach((member) => { |
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 hot path, avoid forEach.
testem.js
Outdated
@@ -8,7 +8,7 @@ module.exports = { | |||
"PhantomJS" | |||
], | |||
"launch_in_dev": [ | |||
"PhantomJS", | |||
// "PhantomJS", |
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 crept in, should remove it.
addon/-private/system/store.js
Outdated
} | ||
|
||
pushed.length = 0; | ||
this._pushedInternalModels = Object.create(null); |
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.
Do we need to reset _hasPushedInternalModels
as well? As far as I can tell, otherwise the _setupRelationships
is only invoked once in the first runloop and never afterwards?
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.
Thanks for the catch! We do need to. I have a fix for this in a different branch where I was rebasing and will bring it back over to here.
@@ -18,4 +18,9 @@ export default class Relationship { | |||
this.hasLoaded = false; | |||
this.meta = null; | |||
} | |||
|
|||
destroy() { | |||
this.currentState = null; |
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.
Check if this is still there
|
||
export default class BelongsToRelationship extends Relationship { | ||
constructor(store, internalModel, inverseKey, relationshipMeta) { | ||
super(store, internalModel, inverseKey, relationshipMeta); | ||
this.kind = 'belongs-to'; |
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 think we camelCase this
newRecord._implicitRelationships[this.inverseKeyForImplicit] = new ImplicitRelationship(this.store, newRecord, this.key, { options: {} }); | ||
} | ||
newRecord._implicitRelationships[this.inverseKeyForImplicit].addRecord(this.record); | ||
// TODO implicit-legacy @runspired is this needed? |
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.
Leftover comment
return record; | ||
}); | ||
return this.store.findBelongsTo(this.internalModel, this.link, this.relationshipMeta) | ||
.then((internalModel) => { |
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.
Can you keep existing indentation please
export default class Relationship { | ||
constructor(store, internalModel, inverseKey, relationshipMeta) { | ||
let asyncOptionValue = relationshipMeta.options.async; | ||
|
||
this.rel_id = REL_ID++; |
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.
Lets leave a comment as to why we are doing this
@@ -31,6 +31,28 @@ const { | |||
|
|||
const assign = Ember.assign || Ember.merge; | |||
|
|||
class UniqueArray { |
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.
Can you document this please, and maybe take it out in a separate file?
this.items = []; | ||
} | ||
|
||
push(...additions) { |
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.
Can we make this more like a util to make it clear it's not a stateful array
if (this.inverseKey) { | ||
this.removeRecordFromInverse(internalModel); | ||
} else { | ||
if (internalModel._implicitRelationships[this.inverseKeyForImplicit]) { |
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.
} else if {
?
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 (this.inverseKey) { | ||
this.removeCanonicalRecordFromInverse(internalModel); | ||
} else { |
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.
} else if {
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
@@ -129,8 +127,7 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { | |||
*/ | |||
this.relationship = this.relationship || null; | |||
|
|||
this.currentState = []; | |||
this.flushCanonical(false); | |||
// this.flushCanonical(false); |
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.
remove?
this.canonicalState = null; | ||
this.kind = 'belongs-to'; | ||
} | ||
get inverseRecord() { |
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.
likely can be removed
return this.currentState; | ||
} | ||
|
||
set inverseRecord(v) { |
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.
likely can be removed
if (this.inverseKey) { | ||
this.removeRecordFromInverse(internalModel); | ||
} else { | ||
if (internalModel._implicitRelationships[this.inverseKeyForImplicit]) { |
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 (this.inverseKey) { | ||
this.removeCanonicalRecordFromInverse(internalModel); | ||
} else { |
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.
👍
return; | ||
} | ||
|
||
this.removeCanonicalRecordFromOwn(internalModel); |
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.
187's index can be reused here, allowing removeCanonicalRecordFromOwn
to avoid the re-scan.
|
||
addInternalModels(internalModels, idx) { | ||
heimdall.increment(addInternalModels); | ||
internalModels.forEach(internalModel => { |
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.
👍
heimdall.increment(newRelationship); | ||
|
||
this.members = new OrderedSet(); | ||
this.canonicalMembers = new OrderedSet(); |
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 create this lazily?
addon/-private/system/store.js
Outdated
if (this._pushedInternalModels.push(internalModel, data) !== 2) { | ||
return; | ||
} | ||
let hash = this._pushedInternalModels; |
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 believe this can be restored, but during _setupRelationships
you should keep a cache of relationshipsByName._key.list
by modelName
around to avoid the lookups bellow.
export default class UniqueArray { | ||
constructor(key) { | ||
this.key = key; | ||
this.seen = Object.create(null); |
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 seems awfully similar to Ember.OrderedSet
We should just tweak that, rather then adding something similar. Note: Ember basically provides OrderedSet
and friends only for ember-data.
I believe we can improve the performance of (small N) ordered set by taking the approach outlined above by @krisselden, it performs quite well for large N.
We should also expose list
as a public attribute on it.
eec7047
to
d9eb9f1
Compare
hrm, that wasn't failing locally. Might have missed something in the commit. |
@@ -429,17 +429,30 @@ export default class InternalModel { | |||
to or has many. | |||
*/ | |||
_directlyRelatedInternalModels() { | |||
let array = []; | |||
this.type.eachRelationship((key, relationship) => { | |||
let uniqueSet = new OrderedSet(); |
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.
all sets are unique, lets just name this variable set
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 name is more so signify the intentional purpose of the set (to generate a unique set), can change if still desired.
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.
best to change it. uniqueSet
is confusing (like hashMapWithKeysAndValues
)
|
||
return this.size; | ||
}; | ||
|
||
OrderedSet.prototype.addWithIndex = function(obj, idx) { | ||
let guid = guidFor(obj); | ||
let presenceSet = this.presenceSet; |
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 is unfortunately private API that will change. Ember-data shouldn't be reaching into private state.. hmmm
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 think the only thing is to add this to ember's, and have ember's lazily create presenceSet
for old versions of ED.
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 part is pre-existing, but I agree. I feel we really just need to build an OrderedSet with good perf and tailored to ember-data, given that it exists inside of Ember mostly for ember-data to begin with.
extend implicit from relationship base relationships off of implicit remove dependency on members from belongs-to relationship remove dependence on members from has-many remove belongs-to dependency on super into implicit decouple belongsTo optimize _directlyRelatedInternalModels remove duplication from Relationship base class cleanup currentState entanglement and begin prepping has-many for decoupling from implicit as a base class test cleanup decouple has-many from implicit remove unneeded code paths from implicit relationships fix(has-many): ensure ManyArray is instantiated lazily optimize setupRelationships clean up has-many noise
…at will need some benchmarking perhaps
…tionships that builds a cache as it goes instead of upfront
rebasing |
fda4dca
to
5abad65
Compare
// This will convert relationships specified as IDs into DS.Model instances | ||
// (possibly unloaded) and also create the data structures used to track | ||
// relationships. | ||
for (let i = 0; i < pushed.length; i += 2) { |
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 produces quite sketchy ES5 output, lots of nested functions to handle the let
TDZ
_setupRelationships: function _setupRelationships() {
var pushed = this._pushedInternalModels;
var store = this;
// Cache the inverse maps for each modelClass that we visit during this
// payload push. In the common case where we are pushing many more
// instances than types we want to minimize the cost of looking up the
// inverse map and the overhead of Ember.get adds up.
var modelNameToInverseMap = Object.create(null);
var modelNameToRelationshipsMap = Object.create(null);
var _loop2 = function _loop2(i) {
var internalModel = pushed[i];
var data = pushed[i + 1];
var modelName = internalModel.modelName;
var relationshipsByName = modelNameToRelationshipsMap[modelName];
if (relationshipsByName === undefined) {
relationshipsByName = modelNameToRelationshipsMap[modelName] = get$3(internalModel.modelClass, 'relationshipsByName');
}
var relationshipNames = relationshipsByName._keys.list;
var relationships = internalModel._relationships;
var _loop3 = function _loop3(keyIndex) {
var relationshipName = relationshipNames[keyIndex];
var relationshipData = data.relationships[relationshipName];
if (relationshipData !== undefined) {
var relationshipRequiresNotification = relationships.has(relationshipName) || isInverseRelationshipInitialized(store, internalModel, data, relationshipName, modelNameToInverseMap);
if (relationshipRequiresNotification) {
relationships.get(relationshipName).push(relationshipData);
}
// in debug, assert payload validity eagerly
}
};
for (var keyIndex = 0; keyIndex < relationshipNames.length; keyIndex++) {
_loop3(keyIndex);
}
};
for (var i = 0; i < pushed.length; i += 2) {
_loop2(i);
}
pushed.length = 0;
}
@@ -324,7 +324,7 @@ export default class RelationshipPayloads { | |||
return; | |||
} | |||
|
|||
if (Array.isArray(data)) { | |||
if (data.length) { // dirty-check for "is-array" |
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.
whats wrong with Array.isArray
?
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'd recommend to stick to Array.isArray
, which gives you predictable performance, unless you now for sure that data
is always an array.
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.
👍
@@ -16,6 +16,18 @@ OrderedSet.prototype = Object.create(EmberOrderedSet.prototype); | |||
OrderedSet.prototype.constructor = OrderedSet; | |||
OrderedSet.prototype._super$constructor = EmberOrderedSet; | |||
|
|||
OrderedSet.prototype.pushMany = function pushMany(additions) { |
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.
unit test
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.currentState = this.canonicalState = internalModel; | ||
this.setupInverseRelationship(internalModel, true); | ||
|
||
// this.flushCanonicalLater(); |
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.
should these be 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.
yes
} | ||
|
||
//TODO(Igor) make this less abysmally slow | ||
this._currentState = this.canonicalState.copy(); |
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.
should we use the currentState
setter?
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.
it doesn't matter here in theory but I did mean to change this to use it once I added the setter.
} | ||
|
||
// in debug, assert payload validity eagerly | ||
runInDebug(() => { |
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 you'd strip the runInDebug
before running Babel on it, then Babel might be able to avoid the closures.
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 still do this, but with #5344 coming and other work including many cleanup commits and lazy-relationships landing over the past year, this has diverged too far from Master. A clean reboot will come soon :) |
Simplified Relationship State
Original Commits https://github.com/runspired/data/tree/original/simple-relationship-state this PR has since been flattened for rebase to continue tracking master.
Description
This is a backwards compatible revisiting of Ember-Data's relationship state objects with the intent of simplifying data flows, improving our ability to reason about the relationship layer, and further optimize relationship management in the future.
How to Review
Each commit in this PR is a discrete step along the path of this refactor, and (with I think one exception) left ember-data in a working state with tests passing. To follow the logic, follow the PRs.
Summary
ImplicitRelationship
class which absorbed most of the previousOrderedSet
behavior ofRelationship
Relationship
class to largely acting as a shared interface thatBelongsToRelationship
HasManyRelationship
andImplicitRelationship
must implement.BelongsToRelationship
andHasManyRelationship
, each now has acurrentState
andcanonicalState
property. Forbelongs-to
these areInternalModel
s while forhas-many
these are arrays ofInternalModel
s.currentState
for theHasManyRelationship
out ofManyArray
and into the relationship class. This bringsManyArray
much closer to being a simple proxy Array.record
=>internalModel
UniqueArray
to make a few performance improvements to places where we were blindly concat-ing arrays leading to duplicate work before.Store._setupRelationships
Motivations
Performance Characteristics
With this refactor we
OrderedSet
s which are only used now forImplicitRelationship
s).OrderedSet
andArray
===
for managingBelongsToRelationship
statediffing
work for each state change within each relationship class layer as well as withinManyArray
These changes were compared against
#master
to validate performance changes we expect to see. The test used thelocalhost:4200/api/complexes?included=foo%2Cbaz&limit=34
endpoint which returns 34 primary records and 204 included records of 3 total model types.Before
After
Results of This PR against LinkedIn Feed