Martin Suchan – BloQ Random #WPdev stuff

4Nov/132

How to debug most common memory leaks on WP8

When developing Windows Phone application, which is more complex than just 3 screens and couple lines of text, you'll probably face the well-known problems of memory leaks. Even when using modern platform as Windows Phone 8, without pointers, with Garbage Collector, IntelliSense and everything, it is still quite easy to experience memory leaks in your apps.

In this article I'll go through 3 most common problems that are causing leaks when developing Windows Phone apps: Images, abandoned Pages and leaks in native controls. I'll also shown you simple trick, how to find your leaks early in the development and not two weeks before project deadline 🙂

So what are memory leaks and why are they unwanted? In short it's a situation when no longer used object is still stored in memory but cannot be accessed by the running code. Together with the fact that there is a memory cap when running on 512MB  Windows Phone 8 devices equal to 150MB, there is a clear potential problem.

Usually when you start average WP8 app, it takes about 10-30MB in memory right after start. Every navigation to new page with text and images takes about 5-10MB of memory. Ideally when navigating back from such pages, the memory should be freed. Unfortunately that's not what is happening all the time, because probably the most common memory leak in Windows Phone apps is when there are abandoned pages in memory.

Let's imagine simple RSS reader app with MainPage - list of articles, and ArticleDetailPage, with article detail. If you are using MVVM for separating the View and ViewModel (you really should), then you probably use some kind of ArticlesViewModel for the list of articles. The simple memory leak, that could happen here, is when you navigate to the ArticleDetailPage, here you subscribe to the ArticleViewModel.PropertyChanged, but you forget to unsubscribe when leaving the page. Every time now when you go to the ArticleDetailPage and back, the page is still kept in memory and eventually your app will crash because there is just too many abandoned pages, that cannot be Garbage collected. So the tip here is simple - make sure each your page is garbage collected after leaving. The simplest solution here is to add Destructor method to your page and log that it's actually being called after navigating back from that page. Simple.

#if DEBUG
~ArticleDetailPage()
{
    Debug.WriteLine("ArticleDetailPage GC");
}
#endif

Second problem you'll most likely encounter is the problem with images. By design all images loaded from Internet and shown in your app are internally cached by OS, for the case they should be displayed again,  to avoid to be re-downloaded again. The problem is obvious, if you have image-heavy application, like app for, you know, browsing online photos, cats, memes, etc., there'll be tens or hundreds of images to be displayed in your app. So how to solve this problem with lot of images kept in memory by system?

You need to create your own UserControl for displaying images, that will also implement IDisposable. You then also need to browse the complete UI element tree when leaving the page, search for your Image UserControls and call Dispose on all of them. In the Dispose method you need first to get the Image.Bitmap that is BitmapImage. Set the UriSource of this BitmapImage to null. Then also set the Image.Source to null as well. This "should" signal the OS, that these images should not be cached any more. More details about this problem and how to solve it is described in this great article. Although it's for WP7, it's still valid for WP8.

Here are some code snippets for the methods you should use there. MoviePosterControl is a UserControl for Image we were using:

// dispose all movie posters
AllMoviesLongListSelector.Descendents<MoviePosterControl>().ForEach(p => p.Dispose());
 
public static IEnumerable Descendents(this DependencyObject root)
    where T : UIElement
{
    int count = VisualTreeHelper.GetChildrenCount(root);
    for (int i = 0; i < count; i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(root, i);
        if (child is T)
        {
            yield return child as T;
        }
        foreach (T descendent in Descendents<T>(child))
        {
            yield return descendent;
        }
    }
}
 
BitmapImage bitmapImage = image.Source as BitmapImage;
bitmapImage.UriSource = null;
image.Source = null;

The third issue you might run into when developing more complex apps, is the problem when native WP8 components are memory leaking as well. That's right, we've experienced in one of our latest projects this behavior - we had Pivot with databound list of PivotItems, each having LongListSelector with items, each with image and text, quite killer combination. We've seen the behavior that user navigated to this page, navigated back, page was garbage collected as expected, but the memory was 10MB more higher. If user did it again, another 10MB of memory. We solved it first by carefully disposing all images used on that page, then we've also replaced databinding with created fixed PivotItems instead, and we've also replaced LongListSelector with simple ItemsControl. Using these "hacks" we were able to reduce the internal leak to be no more than 1MB/navigation there and back.
If you are still curious about this, here's an example conversation with MSFT, that is basically confirming this leak in LongListSelector.

memoryleak

So now we know some typical scenarios where Memory leaks could occur in your app, but how to detect them effectively?

The first most common solution is to use the integrated Memory Profiler in Visual Studio. Or here is another guide on the Nokia Dev Wiki. It works quite fine for detecting some basic memory leaks, but it has some drawbacks - your app must be launched explicitly for this memory analysis. It takes some time after the test run to analyze your report and it might not be obvious in the report where exactly is the problem. And there is also another problem with this method - it cannot find memory leaks caused by the native Windows Phone components! In our mentioned app it showed pretty straight memory usage line, but our app was actually near the memory cap during the run.

The solution I'm going to show you for simple memory analysis is our own Debug popup, that shows the current memory usage on all screens, no matter what are you doing right now. And it's really simple:

 

<UserControl x:Class="MemoryLeak.WP8.DebugInfoPopup"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Popup x:Name="PopupControl" IsOpen="True" Opened="PopupControl_OnOpened">
        <Border Background="#80FFFFFF" Height="60">
            <TextBlock x:Name="MemoryBlock" Foreground="Black" VerticalAlignment="Bottom"/>
        </Border>
    </Popup>
</UserControl>
public sealed partial class DebugInfoPopup
{
    public DebugInfoPopup()
    {
        InitializeComponent();
    }
 
    private readonly DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
    public void PopupControl_OnOpened(object sender, EventArgs e)
    {
        timer.Start();
        timer.Tick += UpdateMemoryInfo;
    }
 
    private void UpdateMemoryInfo(object sender, EventArgs e)
    {
        try
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            // keep the popup open
            PopupControl.IsOpen = true;
        }
        catch (Exception ex)
        {
            // GC.Collect was throwing exceptions in some rare scenarios
            Debugger.Break();
        }
        MemoryBlock.Text = string.Format("Memory: {0:N} bytes", DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage"));
    }
}

Note this UserControl must be added to the MainPage right after it's loaded. The Popup control is then shown on every page. It works like a charm and you see immediately when something's wrong in your app - if you go to a page, memory goes up 8MB, you go back, and the memory still shows 8MB there, then you have a problem. If you fix the issue, run it again and after the navigation back the memory goes down 8MB, you know you have fixed the problem. Even your tester can tell you if the memory is going up suspiciously somewhere, and you don't need to start the profiler and wait for the results to be processed!

You can find a sample of the Debug Popup here as a part of the MS fest sample package, enjoy!