Thursday 8 March 2012

How to monitor garbage collections in a WP7 app

I’m currently working on an app that has the potential to use lots of memory, and I’d like to cache the results as much as I can to speed things up for the user.

I’d also like to get my app certified and available for sale, so must be careful not to eat up all the memory.  One way to do that is with a WeakReference object, but I found that my objects were being garbage collected at the first opportunity, which didn’t really help.

I thought about doing an ephemeral garbage collection by calling GC.Collect(0) on the phone, but that isn’t allowed, and throws an exception.

What I really needed to do was keep my objects alive for a little while after they were used, but I really want to define a little while in terms of the number of garbage collections, rather than running a timer to continually dispose objects.

Unfortunately, I discovered there wasn’t any obvious event raised when a garbage collection is fired, but a little lateral thinking let me realize that there is an event that is called, thought it’s not marked as an event.

It’s actually every object’s finalizer!

So my strategy is to create an object, to which I hold only a weak reference, and when it’s garbage collected I update my statistics, and perform whatever other GC related actions I need to on the UI thread.

To save anyone else the pain, I include the code below.  To fire it up, just reference GCWatcher.GCINFO somewhere in your application, and the notifications begin.

public class GCWatcher
{
/// <summary>
/// static constructor collects the total memory on the device
/// </summary>
static GCWatcher()
{
TotalMemory = (long)DeviceExtendedProperties.GetValue("DeviceTotalMemory");
}

/// <summary>
/// Store a weak reference to our current sacrificial garbage collector object
/// </summary>
static WeakReference _activeGC;

/// <summary>
/// keep a count of the total number of garbage collections seen
/// </summary>
static int gcCount = 0;

/// <summary>
/// store the total memory found on the device
/// </summary>
public static readonly long TotalMemory;

/// <summary>
/// This placeholder should be modified to raise whatever notifications you need in your application
/// It will always be called on the main thread.
/// </summary>
static void Notify()
{
// placeholder for our notification call
}

/// <summary>
/// Property to get the current Garbage Collection info object. If one does not exist it will be created.
/// </summary>
static public GCWatcher GCINFO
{
get
{
WeakReference currentwr = Interlocked.CompareExchange(ref _activeGC, null, null);
GCWatcher current = currentwr == null ? null : currentwr.Target as GCWatcher;
if (current == null)
{
current = new GCWatcher();
Interlocked.Exchange(ref _activeGC, new WeakReference(current));
}
return current;
}
}

/// <summary>
/// Instance property to return the memory usage at the most recent garbage collection
/// </summary>
public long ApplicationCurrentMemoryUsage
{
get;
set;
}

/// <summary>
/// Instance property to return the peak memory usage at the most recent garbage collection
/// </summary>
public long ApplicationPeakMemoryUsage
{
get;
set;
}

/// <summary>
/// protected constructor, to create a new GCWatcher object, and initialize the properties.
/// This is always called on the UI thread.
/// </summary>
internal GCWatcher()
{
// when we create a GC Watcher, we collect the statistics
ApplicationCurrentMemoryUsage = (long) DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage");
ApplicationPeakMemoryUsage = (long) DeviceExtendedProperties.GetValue("ApplicationPeakMemoryUsage");

}

/// <summary>
/// implement a finalizer, that keeps track of our garbage collections via the weak referened object
/// </summary>

~GCWatcher()
{
Interlocked.Exchange(ref _activeGC, null); // zap the value, as we're destroyed
Interlocked.Increment(ref gcCount);

Deployment.Current.Dispatcher.BeginInvoke(
// create a new GCWatcher on the main thread.
() =>
{
WeakReference tmp = new WeakReference(new GCWatcher());
Interlocked.Exchange(ref _activeGC, tmp);
GCWatcher.Notify();
});
}

/// <summary>
/// Thread safe property to return the total counts of garbage collections
/// </summary>
int Count
{
get
{
return Interlocked.CompareExchange(ref gcCount, 0, 0);
}
}

No comments:

Post a Comment