Following on from a previous post on event subscription related memory leaks, I’ve implemented a set of classes that provide Weak Delegate support. They worked quite well in practice and have the following features:
- A WeakDelegate class that allows an object to subscribe to an event but not have the object publishing the event from having to be tied to subscriber objects from a garbage collector point of view.
- A Delegator structure that is used to to define an event field in a publisher object. The Delegator structure provides a means for the developer to choose whether to use the weak delegate or standard event/delegate functionality at run time. The choice is made by setting the Delegator.UseWeakDelegatesByDefault static property to true instead of the default of false. The reason a choice between delegate approaches is useful is that the WeakDelegate implementation does impose a performance hit when compared to the standard event implementation. The performance hit is manageable for some scenarios, so it is quite reasonable to just run with the the WeakDelegate approach for deployment in selected applications.
- The WeakDelegate class provides the ability for an application to be informed whenever an event subscriber has been garbage collected via the WeakDelegate.SubscriberGarbaged event. This provides a means for the developer to identify what parts of their code is missing an event unsubscription call.
- The SubscriberGarbagedArgs information identifies the type of the target object that was garbage collected and which method would have been called on an event occurrence. It can also optionally provide the stack trace current at the time the target object subscribed to the event. The stack trace functionality is activated by setting WeakDelegate.TakeStackTraces to true. The ability to identify the original event subscription code is very useful in helping to work out where the unsubscription code should be.
- The WeakDelegate class has static counter properties (TotalAddCount, TotalRemoveCount, TotalRaiseEventCount and TotalMethodCallEventCount) which assist in identifying how many event add/remove/invoke calls happen when running an application through a typical user scenario. This can help identify whether the WeakDelegate functionality can practically be used at runtime without degrading the effective application performance. The add/remove performance difference varies depending on the number of subscribers per event. Add/remove tests with an average of 10 subscribers have resulted in a 12x decrease in add/remove performance. Invoke tests have resulted in a 600x decrease in invoke performance. Reflection is used to perform the invoke actions because there is no way I have found of replacing the target of a delegate once it has been created. These may seem to be such large factors that an initial conclusion is that its not worth using the WeakDelegate functionality because of the performance issues. In practice it really depends on the number of add/remove/invoke occurrences per unit of time in your application. It may be worth sacrificing some processor cycles for the WeakDelegate implementation if the end user doesn’t have to end up dealing with a memory leak. Its important to note that even if you aren’t willing to take the WeakDelegate performance hit, it is still very useful for identifying where event unscubscription is missing from your code or third party libraries.
- Even if a target object within an event subscription is garbage collected, there will still be the “residue” of a WeakDelegate instance left in the event subscription list. These residue objects are automatically cleaned up the next time the event’s add, remove or invoke methods are called. This residue clean up is the main reason for the decrease in add/remove performance when comparing the standard event/delegate approach to the WeakDelegate implementation.
The sample source code for WeakDelegate functionality has been made available here.
It’s very easy to make an application event leverage the WeakDelegate functionality. An example implementation is shown below. Note that the standard delegate approach will be used by default. To turn on the WeakDelegate functionality, set WeakDelegate.UseWeakDelegatesByDefault = true in your mainline. A useful approach is to only set UseWeakDelegatesByDefault to true in Debug mode for development and then throw an exception if the WeakDelegate.SubscriberGarbaged event is fired. This will help identify scenarios where event unsubscriptions didn’t occur but they should have during development and unit tests.
private Delegator statusChanged;
public event EventHandler StatusChanged
{
add { Delegator.Add( ref statusChanged, value ); }
remove { Delegator.Remove( ref statusChanged, value ); }
}
The WeakDelegate functionality has helped resolve some memory leak issues with a Windows Forms application I am working on. The exercise has lead me to the opinion that I would actually prefer the default event behaviour to weakly reference target objects. If Microsoft could implement a better performing implementation based on access to the CLR innards and compiler modifications, it would allow application programmers to not have to worry about event unsubcription. The reasoning behind this is similar to the reasoning for why we use garbage collectors in general i.e. to let the developer have less to worry about and leverage the available processing power where it’s suitable. Some examples of the potential syntax for such functionality follows:
// Made up syntax example 1.
public weakref event EventHandler StatusChanged;
// Made up syntax example 2.
[ WeakEventReference ]
public event EventHandler StatusChanged;