-
-
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
Free up internalModel
s
#4593
Free up internalModel
s
#4593
Conversation
78872d3
to
88c5287
Compare
// var store = internalModel.store; | ||
// store._dematerializeRecord(internalModel); | ||
|
||
// TODO: internalModel.dematerializeRecord() ? |
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.
@igorT do we actually want to dematerialize on deleted.saved
? seems suspect
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.
per our discussion offline we'll unload the record (which will dematerialize)
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.
@igorT a consequence of this is that we'll destroy deleted records. This may be surprising for eg
let record = store.createRecord('whatever');
record.rollbackAttributes();
// => record is now destroyed
88c5287
to
aad8d7a
Compare
} | ||
}, | ||
|
||
destroy: 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.
concise 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.
👍
@@ -324,6 +383,7 @@ InternalModel.prototype = { | |||
@private | |||
*/ | |||
pushedData() { | |||
this._isDestroyed = 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.
this revives internalModels ?
this._internalModel.recordObjectWillDestroy(); | ||
//TODO should we set internalModel to null here? | ||
|
||
this._internalModel._resetRecord(); |
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.
is there a point to resetting the internal model here, doesn't it just "go dead" once destroyed?
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.
No, the internal model lives on, the record goes dead
@@ -19,6 +19,7 @@ module("integration/adapter/find_all - Finding All Records of a Type", { | |||
firstName: attr('string'), | |||
lastName: attr('string') | |||
}); | |||
Person.reopenClass({ toString: () => 'Person' }); |
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.
concise 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.
👍
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 am curious why this is needed. Which tests are failing without this changes? I would guess only tests wich check for assertions, right? Maybe we should only specify the toString
in those tests...
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.
@pangratz this has no effect on tests passing or failing; it makes debugging tests easier because you can inspect things and get output more useful than "Subclass of DS.Model"
@@ -17,16 +17,25 @@ var Person = DS.Model.extend({ | |||
name: attr('string'), | |||
cars: hasMany('car', { async: false }) | |||
}); | |||
Person.reopenClass({ | |||
toString: () => 'Person' |
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.
👍
return array; | ||
}, | ||
|
||
unloadRecord: 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.
concise 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.
👍
@@ -235,8 +241,68 @@ InternalModel.prototype = { | |||
return this.record; | |||
}, | |||
|
|||
unloadRecord() { | |||
directlyRelatedInternalModels() { | |||
var 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.
let?
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 (allUnloaded) { | ||
relatedInternalModels.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.
why for loop on 281, but forEach here
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.
because of break
}, | ||
|
||
destroy: function() { | ||
this._isDestroyed = true; |
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 mimic embers flags:
isDestroying = true
during distructionisDestroyed = true
once destroyed
this.type.eachRelationship((key, relationship) => { | ||
if (this._relationships.has(key)) { | ||
var related = this._relationships.get(key).members.toArray(); | ||
array.push(...related); |
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 may be fine, but there is an upper limit (in the low hundreds of thousands) that one can push onto an array.
let node = queue.shift(); | ||
array.push(node); | ||
let related = node.directlyRelatedInternalModels(); | ||
related.forEach(function(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.
for loop, for perfy reasons
unloadRecord() { | ||
directlyRelatedInternalModels() { | ||
var array = []; | ||
this.type.eachRelationship((key, relationship) => { |
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 benchmark
var relatedInternalModels = this.allRelatedInternalModels(); | ||
var allUnloaded = true; | ||
var record; | ||
for (var i=0; i < relatedInternalModels.length; 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.
Can you break this method up a bit please
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 write a comment documenting our mark & sweep algorithm
return array; | ||
}, | ||
|
||
allRelatedInternalModels() { |
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 write a short comment describing what this does
@@ -324,6 +383,7 @@ InternalModel.prototype = { | |||
@private | |||
*/ | |||
pushedData() { | |||
this._isDestroyed = 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.
This seems wrong
willDestroy() { | ||
Ember.assert("Cannot destroy an internalModel while its record is materialized", !this.record); | ||
this.store._removeFromIdMap(this); | ||
//other cleanup |
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.
Other cleanup?
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 your comment.
What other cleanup did you have in mind?
this._internalModel.recordObjectWillDestroy(); | ||
//TODO should we set internalModel to null here? | ||
|
||
this._internalModel._resetRecord(); |
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.
No, the internal model lives on, the record goes dead
@@ -584,6 +644,15 @@ InternalModel.prototype = { | |||
}); | |||
}, | |||
|
|||
_resetRecord() { |
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 put a comment saying that this doesn't reset relationships/attributes on purpose, given that constraint, the name is a bit unfortunate
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'll put in a comment, but even so, can you suggest a better name?
@@ -794,6 +794,7 @@ Store = Service.extend({ | |||
|
|||
var resolver = Ember.RSVP.defer('Fetching ' + typeClass + 'with id: ' + internalModel.id); | |||
var pendingFetchItem = { | |||
// TODO: s/record/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.
TODO
@@ -2430,12 +2430,14 @@ Store = Service.extend({ | |||
@param {InternalModel} internalModel | |||
*/ | |||
_dematerializeRecord(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.
Maybe move to the internalModel
}); | ||
|
||
|
||
test('unloading a disconnected subgraph clears the relevant internal models', function(assert) { |
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.
Maybe a test for unloading a part of a connected graph doesn't do that?
allRelatedInternalModels() { | ||
let array = []; | ||
let queue = []; | ||
let seen = []; |
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 consider coloring algo instead of seen. Color via incrementing ID.
}, | ||
|
||
willDestroy() { | ||
Ember.assert("Cannot destroy an internalModel while its record is materialized", !this.record); |
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.
import { assert } from "ember-data/-private/debug";
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.
👍
@@ -47,7 +49,7 @@ testInDebug("unload a dirty record asserts", function(assert) { | |||
} | |||
}); | |||
|
|||
store.findRecord('record', 1).then(function(record) { | |||
store.findRecord('record', 1, { backgroundReload: false }).then(function(record) { |
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 store.findRecord
is unnecessary for this test. Let's use store.peekRecord
instead.
@@ -157,18 +158,23 @@ test("can commit store after unload record with relationships", function(assert) | |||
|
|||
asyncRecords = Ember.RSVP.hash({ | |||
brand: store.findRecord('brand', 1), | |||
product: store.findRecord('product', 1) | |||
product: store.findRecord('product', 1, { backgroundReload: 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.
store.peekRecord
}); | ||
|
||
run(function () { | ||
promise.then(function () { | ||
return store.findRecord('product', 1); |
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.
store.peekRecord
@@ -95,20 +95,18 @@ export default function InternalModel(type, id, store, _, data) { | |||
this.store = store; | |||
this._data = data || new EmptyObject(); | |||
this.modelName = type.modelName; | |||
this.dataHasInitialized = false; | |||
this._loadingPromise = 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.
Seems unused?
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 is written to elsewhere; i have added it here to preserve shape.
Really though we should add a setter and not have other parts of the system write private fields
1ea007b
to
2872536
Compare
I think this issue would close #4126 |
2872536
to
53a22c2
Compare
yes it should |
c2f3d8a
to
7bd94eb
Compare
appveyor build is complaining about downloading phantom. Just need someone to restart. @pangratz this should resolve your concerns @stefanpenner this should resolve your concerns except for |
This looks good to me. 👍 @hjdivad I think @igorT is the only one who can restart an AppVeyor build. So in the past we've been comfortable merged prs when AppVeyor is red and travis is green, if the reason for the AppVeyor failure has to do with a network error during install and there aren't any build changes in the pr. |
Computes the set of internal models reachable from `this` acrosxactly one | ||
relationship. | ||
|
||
@return {Array} An array containing the internal models that `t` belongs |
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.
s/
t/
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.
Also hjdivad#2
this.__implicitRelationships = null; | ||
|
||
// Used for coloring during BFS |
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 likely explain just a-bit more. BFS doesn't help us understand why/where this is used. Likely would be enough to say "during marking phase of unloading"
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.
👍
@@ -278,8 +290,22 @@ export default class InternalModel { | |||
this._triggerDeferredTriggers(); | |||
} | |||
|
|||
recordObjectWillDestroy() { | |||
resetRecord() { | |||
this.record = 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.
is there actual value in reseting the record during destroy, or can we just let it go 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.
@stefanpenner the goal is to at least do enough to ensure
- The internal model doesn't keep the record alive
- The internal model knows the record is dematerialized
- The internal model is able to resurrect the record
} | ||
|
||
dematerializeRecord() { | ||
if (this.record) { |
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.
Curious, should we assert in development if we attempt to dematerialize something already dematerialized? Wouldn't it mean we are doing duplicate work?
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 case where we wind up here with !this.record
is store.unloadAll(type)
which is public API.
We could change this to assert !!this.record
and then tweak the store to only unload materialized internal models. thoughts?
let array = []; | ||
this.type.eachRelationship((key, relationship) => { | ||
if (this._relationships.has(key)) { | ||
let related = this._relationships.get(key).members.toArray(); |
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 create this array, to just iterate then throw it away?
@hjdivad do you mind rebasing this pr? |
ping @hjdivad |
ping @hjdivad, if you don't have time (as I suspect you don't, hi office budy! 👋 ) just say so and I'll pull down your work and try to move it along. |
Any updates on this? I'm running into just this problem. Is there any brute force method to get by this until it has been fixed? |
@runspired @bmac gonna try to get to it this weekend @runspired i'll likely need to sync up with you re your recent changes in this area |
@hjdivad I may have to come also crash at your place :P @trumb1mj I don't think you are hitting this issue exactly but a different retention issue that's related. That said, our intention is to make it so that internal models ARE kept if a relationship still has access to them, while your need is explicitly for them to be unloaded even when one does. David @igorT and myself will need to consider this case. |
@runspired is there any way to build my relationships to avoid this? Also, I don't understand why deleting an object with a many-to-1 would cause any residual references to be left around--the other way makes a bit of sense but I still think we should be able to opt out and just whack things. Not disagreeing just not quite understanding. |
c5f22fb
to
fd8d42b
Compare
this.currentState = RootState.empty; | ||
this.isReloading = false; | ||
// TODO: see if we can clean this up | ||
this._isDestroying = 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.
Either way lets leave a comment on why/how it's used
this._isUpdatingRecordArrays = false; | ||
this._isDematerializing = 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.
Same here
getRecord() { | ||
if (!this._record) { | ||
if (!this._record && !this._isDematerializing) { |
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 comment this
if (internalModel._bfsId < bfsId) { | ||
queue.push(internalModel); | ||
internalModel._bfsId = bfsId; | ||
} |
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.
would it be worth adding a dev assert if we ever see a case of internalModel._bfsId > bfsId
? That would mean bad things are happening
} | ||
|
||
_cleanupOrphanedInternalModels() { | ||
this._isDematerializing = 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.
Should comment on why we do this here
assert("Cannot destroy an internalModel while its record materialized", !this.record || this.record.get('isDestroyed') || this.record.get('isDestroying')); | ||
|
||
this.store._removeFromIdMap(this); | ||
this._isDestroyed = true; |
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 set isDestroying to 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.
If we still need it
this._internalModel.clearRelationships(); | ||
this._internalModel.recordObjectWillDestroy(); | ||
//TODO should we set internalModel to null here? | ||
this._internalModel.resetRecord(); |
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.
Seems odd to call this from the record into the internalModel. Can we move it somewhere else?
fddb4cb
to
b387340
Compare
`internalModel`s need to stay around as long as they're connected to a live record via relationships. Once an entire subgraph is removed however, we can free all of it. Also add a couple of "late private" fields to `internalModel`'s constructor for shape preservation. Finally - Add a lot of `toString`s to test classes, as a debugging convenience. - Minor test cleanup Includes work from @igorT, @sly7-7 and @pangratz [Fix emberjs#3296] [Supercede emberjs#3301]
b387340
to
2f18405
Compare
Did this seem to leave anybody else with severe performance problems? In previous versions of Ember Data it was relatively quick to |
@Kerrick can you open a fresh issue and provide a reproduction. Sounds like something we should look into and that will make this actionable |
@Kerrick I'm also experiencing exactly the same issue. 2.12 was super fast. 2.13-2.15 is so slow at unloading the records that it seems to hang the browser. I've also noticed given certain relationships lead to a memory leak that quickly grows to the gigabyte range of memory in a minute or two and then crashes. |
internalModel
s need to stay around as long as they're connected to a live record via relationships. Once an entire subgraph is removed however, we can free all of it.Also add a couple of "late private" fields to
internalModel
's constructor for shape preservation.Includes work from @igorT and @sly7-7
[Fix #3296]
[Supercede #3301]