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

Fix listenTo memory leak #3455

Merged
merged 16 commits into from
Feb 3, 2015
Merged

Conversation

jridgewell
Copy link
Collaborator

Fixes #3453, and competes with #3049.

I think this aligns more closely with what's currently implemented. 😄

It also implements listenApi, which is eventsApi translated to the listenTo / stopListening method signatures.

@@ -79,7 +79,7 @@
test("listenTo and stopListening with event maps", 4, function() {
var a = _.extend({}, Backbone.Events);
var b = _.extend({}, Backbone.Events);
var cb = function(){ ok(true); };
var cb = function(){ console.log('called'); ok(true); };
Copy link
Collaborator

Choose a reason for hiding this comment

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

Whoops :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

😄

@jridgewell
Copy link
Collaborator Author

As a side: I have a completed eventsApi refactor that cleans up the duplication in listenApi. I haven't included it in this PR yet, since it might be polarizing. If the only issue with this PR is the duplication, I'd be glad to put it in as well.

@jashkenas
Copy link
Owner

Cleaning up the duplication would be lovely.

@jridgewell
Copy link
Collaborator Author

Ok, it's included now.

To be clear, I think the trigger with object behavior is silly. But I
needed tests to ensure that my [`eventsApi` refactor]
(https://github.com/jridgewell/backbone/tree/eventsApi-refactor) kept
the current functionality.
@jridgewell jridgewell force-pushed the listenTo-memory-leak branch from 6ad0063 to 59c3704 Compare January 26, 2015 21:55
// Maps the normalized event callbacks into onceWrappers.
var onceMap = function(name, callback, offer) {
return eventsApi(function(map, name, callback, offer) {
if (callback) map[name] = onceWrap(name, callback, offer);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason not to inline onceWrap here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope. I'll fix it.

@jridgewell
Copy link
Collaborator Author

Ping @akre54: I commented, and added a triggerSentinel object to prevent further confusion. Let me know you have any other suggestions.

if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);

// Use `eventsApi` to normalize `name` into the proper event names.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mind clarifying this? I'm not sure what this is getting at.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jridgewell the comments are still lacking. Can you do a pass to make sure the logic is clear? I'm finding it hard to follow what is happening and why.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't revised the comments yet, was trying to solve the off problem first.

Is there a particular spot you're not able to follow?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This line in particular isn't clear what it does, but the bits with triggerSentinel and the numerous internal functions mostly.

It almost seems like we should drop the Events = {on: ..., off: ..., listenTo: ...} object, and just colocate the different events methods (Events.on = ...; onApi = ...; Events.off = ...;.) To follow this logic, I'm having to jump back and forth through the source to even understand where the different pieces are going.

There's so much extra overhead and indirection going on here, it really detracts from the readability of the Events code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree the source could be better arranged, I'll work on that tonight along with the comments.

There's so much extra overhead and indirection going on here

I actually thought it was kind of easier to follow without the old eventsApi behavior, but then I had to account for off cleaning up any listeners. It makes it easier in my mind to think of all the *Api functions as special purpose reduce iteratees, with eventsApi being _.reduce.

@akre54
Copy link
Collaborator

akre54 commented Jan 27, 2015

I think a little more of the "why" and the "how" and less "what" in the comments might be helpful.

Maybe a sentence or two at the top briefly describing how the many abstracted functions work. Right now I'm on the fence that this consolidation is worth the added indirection. It definitely makes it harder to follow the logic.

Can you also make a jsperf showing the extra functions aren't too horrible and squash to a single commit and I'll merge? Thanks for your hard work on this. (And no stress about getting it done today... enjoy the snowday!)

@jridgewell
Copy link
Collaborator Author

Here's a simple jsperf: http://jsperf.com/eventsapi-refactor (It was hidden in 50adef3's commit message). Safari seems to be the only one taking a hit, but it's still very fast.

@akre54
Copy link
Collaborator

akre54 commented Jan 27, 2015

how about vs master? That's showing all the changes with the extra
callbacks added in.

On Tue Jan 27 2015 at 10:42:24 AM Justin Ridgewell notifications@github.com
wrote:

Here's a simple jsperf: http://jsperf.com/eventsapi-refactor (It was
hidden in 50adef3
50adef3c03841038a706427591cfeccc1ad280d8's
commit message). Safari seems to be the only one taking a hit, but it's
still very fast.


Reply to this email directly or view it on GitHub
#3455 (comment).

@jridgewell
Copy link
Collaborator Author

Here's the simple case against master, and another using space-separated events.

I did the original tests against the listenTo-memory-leak-immutable-on-events branch because that's the one with the minimum code to fix the memory leak. Comparing against master seemed apples-to-oranges.

@jridgewell
Copy link
Collaborator Author

Actually, the performance hit seems to be coming from #3463, which is also merged into eventsApi-refactor. The simple case against those two have the exact same speeds.

@melnikov-s
Copy link
Contributor

Nice, but does not seem to compete with #3049 as it does not address the following:

var a = _.extend({}, Backbone.Events);
var b = _.extend({}, Backbone.Events);
var c = _.extend({}, Backbone.Events);

a.listenTo(c, 'x', fn);
b.listenTo(c, 'x', fn);
c.off('x');
c = null; //c can't be garbage collected until 'stopListening()' is called on a and b

Here are the relevant tests

 test("off removes object from listeners", 6, function() {                                                                                                                                                                                    
    var a = _.extend({}, Backbone.Events);
    var b = _.extend({}, Backbone.Events);
    var c = _.extend({}, Backbone.Events);
    var d = _.extend({}, Backbone.Events);
    var fn = function() {};    

    b.listenTo(a, 'all', fn);  
    c.listenTo(a, 'all', fn);  
    c.listenTo(d, 'x', fn);    

    equal(_.size(b._listeningTo), 1);
    equal(_.size(c._listeningTo), 2);
    equal(_.size(a._listeners), 2); 
    a.off();                   
    equal(_.size(b._listeningTo), 0);
    equal(_.size(c._listeningTo), 1);
    equal(_.size(a._listeners), 0); 
  }); 

  test("off w/event name removes object from listeners", 8, function() {
    var a = _.extend({}, Backbone.Events);
    var b = _.extend({}, Backbone.Events);
    var c = _.extend({}, Backbone.Events);
    var fn = function() {};

    b.listenTo(a, 'x', fn);
    c.listenTo(a, 'x y', fn);

    equal(_.size(b._listeningTo), 1);
    equal(_.size(c._listeningTo), 1);
    equal(_.size(a._listeners), 2); 
    a.off('x');
    equal(_.size(b._listeningTo), 0);
    equal(_.size(c._listeningTo), 1);
    equal(_.size(a._listeners), 1); 
    a.off('y');
    equal(_.size(c._listeningTo), 0);
    equal(_.size(a._listeners), 0); 
  });

@jridgewell jridgewell force-pushed the listenTo-memory-leak branch from ad3e065 to c7309a3 Compare January 29, 2015 16:06
@jridgewell
Copy link
Collaborator Author

I've addressed @smelnikov's #off memory leak, but the code isn't as pretty as I would have liked.

@jridgewell jridgewell force-pushed the listenTo-memory-leak branch from c7309a3 to f28fca1 Compare January 29, 2015 16:21
This is to prevent an infinite off -> stopListening -> off ->
stopListening.
@jridgewell jridgewell force-pushed the listenTo-memory-leak branch from f28fca1 to 826b110 Compare January 29, 2015 16:28
var ids = context != null ? [context._listenId] : _.keys(listeners);
for (var i = 0, length = ids.length; i < length; i++) {
var listener = listeners[ids[i]];
if (!listener) break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this necessary? I would expect a continue if anything

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When you listenTo, context is forced to be this. If we're offing, we need to consider two circumstances:

  1. No context
    • We'll loop through all our listeners, since any could be affected.
    • This conditional will never be true, so we'll never break.
  2. Context
    • Only loop through the listeners of the corresponding _listenId, since only that listener will have the same context
    • If we have listeners, but not that _listenId, then break.
      • ids is guaranteed to only equal [context._listenId], so there's no use continueing.
    • If we have that _listenId, all is good.

@jridgewell
Copy link
Collaborator Author

@akre54 Commented and rearranged.

@jashkenas
Copy link
Owner

I think let's maybe merge this for now -- and then go over it and tweak as need be. (But hopefully need not ;)

jashkenas added a commit that referenced this pull request Feb 3, 2015
@jashkenas jashkenas merged commit 713c288 into jashkenas:master Feb 3, 2015
jameshartig pushed a commit to mc0/bedrock that referenced this pull request Feb 12, 2015
We should eventually pull in their changes but for a hope in actually
getting this fix in production I opted to do this instead for now.
jameshartig pushed a commit to mc0/bedrock that referenced this pull request Feb 12, 2015
We should eventually pull in their changes but for a hope in actually
getting this fix in production I opted to do this instead for now.
@jridgewell jridgewell mentioned this pull request May 2, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory leak in stopListening?
5 participants