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

Free up internalModels #4593

Merged
merged 1 commit into from
Jan 16, 2017
Merged

Free up internalModels #4593

merged 1 commit into from
Jan 16, 2017

Conversation

hjdivad
Copy link
Member

@hjdivad hjdivad commented Oct 19, 2016

internalModels 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]

// var store = internalModel.store;
// store._dematerializeRecord(internalModel);

// TODO: internalModel.dematerializeRecord() ?
Copy link
Member Author

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

Copy link
Member Author

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)

Copy link
Member Author

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

@hjdivad
Copy link
Member Author

hjdivad commented Oct 19, 2016

}
},

destroy: function() {
Copy link
Member

Choose a reason for hiding this comment

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

concise function

Copy link
Member Author

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

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

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?

Copy link
Member

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

Choose a reason for hiding this comment

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

concise function.

Copy link
Member Author

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

Copy link
Member Author

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

👍

return array;
},

unloadRecord: function() {
Copy link
Member

Choose a reason for hiding this comment

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

concise function

Copy link
Member Author

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

Choose a reason for hiding this comment

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

let?

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 (allUnloaded) {
relatedInternalModels.forEach((internalModel) => {
Copy link
Member

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

Copy link
Member Author

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;
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 mimic embers flags:

  • isDestroying = true during distruction
  • isDestroyed = true once destroyed

this.type.eachRelationship((key, relationship) => {
if (this._relationships.has(key)) {
var related = this._relationships.get(key).members.toArray();
array.push(...related);
Copy link
Member

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

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) => {
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 benchmark

var relatedInternalModels = this.allRelatedInternalModels();
var allUnloaded = true;
var record;
for (var i=0; i < relatedInternalModels.length; i++) {
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 break this method up a bit please

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 write a comment documenting our mark & sweep algorithm

return array;
},

allRelatedInternalModels() {
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 write a short comment describing what this does

@@ -324,6 +383,7 @@ InternalModel.prototype = {
@private
*/
pushedData() {
this._isDestroyed = false;
Copy link
Member

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

Choose a reason for hiding this comment

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

Other cleanup?

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

What other cleanup did you have in mind?

this._internalModel.recordObjectWillDestroy();
//TODO should we set internalModel to null here?

this._internalModel._resetRecord();
Copy link
Member

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() {
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 put a comment saying that this doesn't reset relationships/attributes on purpose, given that constraint, the name is a bit unfortunate

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

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

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

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 = [];
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 consider coloring algo instead of seen. Color via incrementing ID.

},

willDestroy() {
Ember.assert("Cannot destroy an internalModel while its record is materialized", !this.record);
Copy link
Member

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";

Copy link
Member Author

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

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

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

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

Choose a reason for hiding this comment

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

Seems unused?

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

@wecc
Copy link
Contributor

wecc commented Oct 21, 2016

I think this issue would close #4126

@hjdivad
Copy link
Member Author

hjdivad commented Oct 22, 2016

I think this issue would close #4126

yes it should

@hjdivad
Copy link
Member Author

hjdivad commented Oct 22, 2016

appveyor build is complaining about downloading phantom. Just need someone to restart.

@pangratz this should resolve your concerns
@igorT this should resolve your concerns except for deleting implicitly unloading: we should do this in a separate effort.

@stefanpenner this should resolve your concerns except for eachRelationship vs eachRelationshipByName. That shouldn't block the PR; we can switch it after doing some benchmarks

@bmac
Copy link
Member

bmac commented Oct 24, 2016

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

Choose a reason for hiding this comment

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

s/t/this/?

Copy link
Member

Choose a reason for hiding this comment

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

Also hjdivad#2

@hjdivad
Copy link
Member Author

hjdivad commented Oct 25, 2016

@bmac turns out I can also restart an AppVeyor build by writing comments with such atrocious spelling that @pangratz has to save me with a new commit.

this.__implicitRelationships = null;

// Used for coloring during BFS
Copy link
Member

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"

Copy link
Member Author

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

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.

Copy link
Member Author

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

  1. The internal model doesn't keep the record alive
  2. The internal model knows the record is dematerialized
  3. The internal model is able to resurrect the record

}

dematerializeRecord() {
if (this.record) {
Copy link
Member

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?

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 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();
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 create this array, to just iterate then throw it away?

@bmac
Copy link
Member

bmac commented Oct 28, 2016

@hjdivad do you mind rebasing this pr?

@bmac
Copy link
Member

bmac commented Nov 11, 2016

ping @hjdivad

@runspired
Copy link
Contributor

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.

@trumb1mj
Copy link

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?

@hjdivad
Copy link
Member Author

hjdivad commented Jan 11, 2017

@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

@runspired
Copy link
Contributor

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

@trumb1mj
Copy link

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

@hjdivad hjdivad force-pushed the better-unload branch 3 times, most recently from c5f22fb to fd8d42b Compare January 15, 2017 21:49
this.currentState = RootState.empty;
this.isReloading = false;
// TODO: see if we can clean this up
this._isDestroying = false;
Copy link
Member

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

getRecord() {
if (!this._record) {
if (!this._record && !this._isDematerializing) {
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 comment this

if (internalModel._bfsId < bfsId) {
queue.push(internalModel);
internalModel._bfsId = bfsId;
}
Copy link
Member

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

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;
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 set isDestroying to false?

Copy link
Member

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

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?

@hjdivad hjdivad force-pushed the better-unload branch 3 times, most recently from fddb4cb to b387340 Compare January 16, 2017 06:19
`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]
@Kerrick
Copy link
Contributor

Kerrick commented May 5, 2017

Did this seem to leave anybody else with severe performance problems?

In previous versions of Ember Data it was relatively quick to store.unloadAll('my-model-type') when I had ~2500 of them loaded, but in Ember Data 2.12 it takes upwards of 25 seconds. (In case it's relevant, my model has 5 hasMany relationships, and 3 belongsTo relationships.)

image

image

@stefanpenner
Copy link
Member

@Kerrick can you open a fresh issue and provide a reproduction. Sounds like something we should look into and that will make this actionable

@tylerturdenpants
Copy link
Member

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

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.

None yet

10 participants