Background

Event leaks are a big pain in DeltaShell (or any application). With an event leak I mean when a listener (typically IView, BindingList) listens to an object it should no longer know/care about (Data).

One important convention with regard to unregistering/disposing resources in DeltaSehll is the logic around IView.Data.

When the view opens, its data is set:

IView.Data = dataObject; //view registers to dataObject

The view registers to the dataObject and presents it in the view. When the view is closed by the user, the Data is set to null, which tells the View it should unregister from the previous Data:

IView.Data = null; //view must unregister from previous dataObject

If a view forgets to dispose/unregister from all events, we have an event leak. And these event leaks typically cause memory leaks, crashes and performance degradation (see example).

Example

Suppose a FunctionView does not unregister from a function at all when the view is closed. If the function changes, the event listeners in the closed view will be called. The garbage collector notices the dependency and will keep the view available in memory (memory leak). The event listeners code may update the closed / data-less and potentially disposed view: good chance of crashes. And finally the code in the event listener may trigger heavy updates, like updating the chart axis on every value added to the function (performance degradations).

Step 1 - Don't create them

Hopefully dangers of event leaks are clear now, so what can we do about them? Unfortunately it's very easy to create event leaks, but most views in DeltaShell follow this useful pattern which helps to prevent mistakes:

public void Data
{ 
    get { return data;}
    set
    {
        if (data != null)
        {
             Unsubscribe(data); //unregister from any old data
        }

        data = value; //set the data

        if (data != null)
        {
            Subscribe(data); //register to new data
        }
    }
}

This keeps things clean (as long as Unsubscribe and Subscribe are in sync). Some views however create intermediate objects, such as (Function)BindingLists, which complicate things. As a rule of thumb, each time you do '+=' or 'new FunctionBindingList' (or similar object), you should have a '-=' and a 'Dispose' to match it. Same goes for DelayedEventHandlers etc. Check your disposables!

Composite views must be careful to also set their children's Data to null when their own Data is set to null.


Step 2 - Detect & alert

So how do you know if your view is without event leaks? Although not a guaranteed approach, there is a test helper which can be of use. You can use this test helper to debug and detect any existing leaks, and also use it as a shotgun-test to alert you if someone introduces a event leak afterwards.

To use the helper (TestReferenceHelper), the pattern is typically as follows:

[Test]
public void ViewShouldUnregisterFromData()
{
    var testObject = new TestClass(); //your data object
    var view = new MyView(); //your view

    int numSubscriptionsBefore = TestReferenceHelper.FindEventSubscriptions(testObject);

    view.Data = testObject;  //at this point numSubscriptions is typically higher: the view is registered to events on the data
    view.Data = null;  //at this point the view should have unsubscribed again..

    int numSubscriptionsAfter = TestReferenceHelper.FindEventSubscriptions(testObject);

    //assert no leaks:
    Assert.AreEqual(numSubscriptionsBefore, numSubscriptionsAfter); //if the view did its job, these should be equal
}

The method searches the object and its properties (up to some depth) for event subscriptions. There are some overloads on the method that allow you to see the actual subscriptions for debugging purposes. That output looks a bit like this:

{PostSharp}.PropertyChanging
{PostSharp}.PropertyChanged
.Components.CollectionChanged
.Components.PropertyChanged
.Components.CollectionChanged (x4)
.Components.CollectionChanging (x4)
//etc..

You can place this output from before and after in a file comparer to quickly find the leaked events (there's no build-in difference viewer, so that's manual work).

That's it. But while you're at it, you may want to check out another method in TestReferenceHelper: SearchObjectInObjectGraph(target,graph). Check out the usages of this method so see how it can help you to test/debug your Clone/DeepClone methods.

If there are any questions / suggestions please let me know!

  • No labels

2 Comments

  1. Nice post, thanks!

    See also Smart way to handle events which has some DO and DONT tips.

    Suggestions / comments:

    • Move this method into TestHelper - much easier to find when all these test tools are at the same place.
    • Composite views must set child view data to null - we can avoid this (YAGNI) for views which implement ICompositeView. In this case it can be implemented in GuiCommandHandler.RemoveAllViewsForItem(object dataObject), once and for all. 
    • What to do with non-PostSharp events?
    • Looks like it is something to integrate into Coding Conventions
  2. Non-PostSharp events are also found!