Tracking BitmapSource Memory Usage

A BitmapSource object in WPF often represents a large amount of native memory (to hold the bitmap data). This memory is not tracked by the .NET Garbage Collector (because it’s native, not managed) and is only freed once the BitmapSource is collected, the underlying SafeMILHandle is released, and the finalizer thread cleans up the native resources.

For an application that creates a lot of bitmaps, garbage collection may not happen frequently enough for the managed objects to be collected and the native objects to be freed; this will cause an OutOfMemoryException to be thrown.

One could call GC.Collect frequently to fix this, but that is wrong. The right thing to do is to help the GC tune its collection algorithm by calling GC.AddMemoryPressure to inform it of the native memory allocation. WPF 4 does this; WPF 3.5 does not, so we need to implement a workaround.

I built a solution using a watchdog thread and WeakReferences, but Ed suggested an alternate approach that leverages the finalizer thread and works quite nicely.

We create an attached DependencyProperty for the BitmapSource type; the value of this property is a finalizable “MemoryUsage” object that simply represents the number of bytes the BitmapSource it’s attached to is using. The MemoryUsage constructor adds memory pressure, and its finalizer removes it. The hope is that the BitmapSource’s SafeMILHandle and the MemoryUsage object will be finalized around the same time (some time after the owning BitmapSource is collected), and thus the delay between freeing the native memory and informing the GC that it was freed will be as short as possible. (This is very similar to how WPF 4 works, except that it chains these two operations together in the SafeMILHandle finalizer.)

UPDATE: An attached DependencyProperty cannot be set on a frozen DependencyObject, so the memory pressure must be added before the BitmapSource is frozen. The return value from BitmapFrame.Create is always frozen; if you’re using large BitmapFrames, the watchdog thread may be a better solution.

public static class BitmapUtility
{
    /// <summary>

    /// Reports the memory used by <paramref name="bitmap"/> to the GC.

    /// </summary>

    /// <param name="bitmap">The bitmap.</param>

    public static void AddMemoryPressure(BitmapSource bitmap)
    {
        // "stride" is bytes used per bitmap scanline

        int stride = bitmap.PixelWidth * ((bitmap.Format.BitsPerPixel + 7) / 8);
        int height = bitmap.PixelHeight;
        bitmap.SetValue(MemoryUsageProperty, new MemoryUsage((long) stride * height));
    }

    // MemoryUsage adds GC memory pressure in its constructor, and removes it in its finalizer.

    private sealed class MemoryUsage
    {
        public MemoryUsage(long bytesAllocated)
        {
            m_bytesAllocated = bytesAllocated;
            if (bytesAllocated > 0)
                GC.AddMemoryPressure(m_bytesAllocated);
            else
                GC.SuppressFinalize(this);
        }

        ~MemoryUsage()
        {
            GC.RemoveMemoryPressure(m_bytesAllocated);
        }

        readonly long m_bytesAllocated;
    }

    static readonly DependencyProperty MemoryUsageProperty = DependencyProperty.RegisterAttached("MemoryUsage", typeof(MemoryUsage),
        typeof(BitmapUtility), new PropertyMetadata(null));
}

Posted by Bradley Grainger on August 31, 2010