Sunday, May 2, 2010

Bindings & Observers don't deactivate on destroy

Currently in Sproutcore, bindings & remote observers (observers on other objects) are not deinitialized when the object is destroyed.

In the case of both bindings & remote observers, the remote object ends up with a reference to this object. Hence, this object does not get garbage collected & remains around till the remote object becomes eligible for GC.

If the remote object is a controller, chances are that it remains around for the lifetime of the application. Hence, this object also remains around in a destroyed state but still consuming memory, till the browser window is closed.

For a long running application, the browser accumulates a huge amount of memory over a period of time.

More importantly, since the bindings & observers are still active, they may interfere with bindings & observers on newer objects representing the same data. This can lead to unpredictable results.

This issue manifests only if your application is long running or keeps creating and destroying objects for the same data periodically.

Currently there is no decent solution for this, as Javascript has neither weak references nor lifecycle callbacks for an object. So its not possible to have references without preventing garbage collection & there is no guaranteed place to remove the hard references.

Views are the objects that are usually created and destroyed the most in any application & they have the most number of bindings to long-living objects like the controllers. So if we are able to solve this problem for views atleast, it reduces the problem greatly.

As it turns out, there is a workaround:

1. Lifecycle of views is completely controlled by sproutcore. So the destroy() method almost always gets called on views once they are removed.

2. All bindings created using the approach:
  myPropNameBinding: 'MyApp.Foo.x'
are maintained in an array called "bindings" within the object itself. We can iterate through the array in destroy() and disconnect the bindings & release the references.

Unfortunately, there is no straightforward approach to deinitialize observers. But local observers don't cause a remote reference and hence don't hold back the object from being garbage collected.

So, if we ensure that we always use local observers (observers on properties of the same object) only, then we can ensure that remote connections between views and other objects are cleaned up properly, just by disconnecting bindings.

So instead of writing something like

MyApp.Foo = SC.View.extend({
  myObesrvingMethod: function() {
    ...
  }.observes('MyApp.Bar.xyz')
});

you need to create a binding to the remote object & then create a local observer on it:

MyApp.Foo = SC.View.extend({
  myBarXyz: null,
  myBarXyzBinding: 'MyApp.Bar.xyz',

  myObesrvingMethod: function() {
    ...
  }.observes('myBarXyz')
});

Then, on all these views, the destroy() method can be overridden along the lines below:

  destroy: function() {
    this.bindings.forEach(function(binding) {
      binding.disconnect();
    });
    this.bindings = null;
    sc_super();
  }

1 comment:

  1. Thanks for posting about this, and it's a hideous oversight/bug in SproutCore. A while ago, the item views of collection views were not getting properly destroyed either, which had some performance implications (I'm not sure if they've addressed this issue.)

    On a related note, there's a Cleanup mixin that disconnect bindings for views that are not currently visible.

    http://github.com/etgryphon/sproutcore-ui/blob/master/frameworks/foundation/mixins/cleanup.js

    ReplyDelete