Tracking Memory Leaks In Xamarin With The Profiler – Part 1

Memory leaks in a mobile application, is the result of objects being held in memory when they should be disposed of. This occurs when a reference to that object, still exists. While your application has a reference to that object, the GC (Garbage Collector), will not remove it from memory. This is great, but if your application no longer needs it, it should be removed. The worst case scenario, is the memory continues to grow, until your mobile device is out of memory, and the OS closes your app.

Memory leaks can be notoriously hard to track down. While some cases are easy, such as you forgot to unsubscribe from an event, others are far sneakier, and can cost days in tracking them down.

Xamarin Profiler Setup

For this walkthrough, I will be running a Xamarin Forms iOS app, using Visual Studio 2015 Enterprise, on Windows 10, with the Windows Xamarin Profiler and the Remote iOS Simulator, just because I like living on the edge. Ideally this is my preferred setup, however its not always stable.

If you haven’t used the profiler before, please read Xamarin Forms Profiling. I will be using the Memory Instrument, to see if I can track down a purposely made memory leak, in my project.

Project Setup

The first thing we need to do, is setup a bare basics project. I have already established that there is a leak somewhere, but in order to make it easy to find, we need to start of at the minimum amount of code. I have just created a sample project and directly referenced the Xamarin Forms Source Code.

I have one NavigationPage and two ContentPages. I navigate back and forth between each one, and the memory should stay consistent. However, I am going to add in a memory leak, by keeping a reference of the page in a static list, every time it is created.

Application

In the main App.xaml.cs, I am placing a static list, to hold instances of pages, and define the navigation page.

public partial class App : Application
{
    public static List<Page> MemoryLeak = new List<Page>();

    public App()
    {
        MainPage = new NavigationPage(new Sample.MainPage());
    }
}

Main Page

The main page is the starting page of the app, it also holds a label, which displays the current memory usage of the app. This is just there for reference, and a second opinion.

public partial class MainPage : ContentPage
{
    private Label _memoryLabel;
    private bool _isMonitoring = false;
    public MainPage()
    {
        this.Padding = 20;
        this.Title = "Main Page";

        var navigateButton = new Button() { Text = "Navigate To Next Page" };

        var stackLayout = new StackLayout();
        _memoryLabel = new Label();

        navigateButton.Clicked += (s, e) =>
        {
            ((NavigationPage)Application.Current.MainPage).PushAsync(new SecondPage());
        };

        stackLayout.Children.Add(_memoryLabel);
        stackLayout.Children.Add(navigateButton);

        this.Content = stackLayout;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        _isMonitoring = true;
        Device.StartTimer(TimeSpan.FromSeconds(1), () =>
        {
            GC.Collect();
            _memoryLabel.Text = GC.GetTotalMemory(true).ToString("N0");
            return _isMonitoring;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        _isMonitoring = false;
    }
}

Second Page

The second page just has a ListView and Label. I put these extra controls here, just to bulk out the page a little, because memory leaks at this level will be in the bytes per page, and hard to see otherwise.

public partial class SecondPage : ContentPage
{
    public SecondPage()
    {
        this.Padding = 20;
        this.Title = "Second Page";

        var label = new Label() { Text = "Welcome to the second page" };

        var stackLayout = new StackLayout();
        stackLayout.Children.Add(label);

        stackLayout.Children.Add(new ListView());

        this.Content = stackLayout;

        // Where the leak will occur
        App.MemoryLeak.Add(this);
    }
}

Profiling

Ensure you have setup your application for profiling. I have set the iOS Project as the Startup Project. Once done, go to Analyze > Xamarin Profiler.

analyze

Then select the Memory Instruments.

memory-instrument

Once the profiler loads, you will see the Allocations and Cycles instruments data start to appear. The allocations will increase, until the app is loaded. It will show the Max MB’s that have been allocated. Please note that I have found this number to be highly inaccurate in detecting memory movements, and show a continuous upward trend, when the label I added did not.

profiler-loaded

Snapshot

Snapshots, are a detailed record of the allocations and memory usage at a specific point in time. You will want to press the snapshot button once your app has loaded, to get a baseline.

take-snapshot

Then, lets navigate to the second page, and press back again. I would create another snapshot now. Then I might keep on doing the navigation a few more times and pressing snapshot again. After a few snapshots you will see a screen similar to this.

multiplesnapshots

You have two columns to take note of. First is the object count, the second is the size growth. If they are red, they have increased since the previous snapshot. This is what snapshots are used for, noting detailed differences between certain points in time, in your app.

Note: Please note that the GC, can come in and remove many objects. You may still have a leak, even if the memory count for that snapshot, shows a downward trend.

Snapshot Differences

If you double click on a snapshot, it will show you all new objects. You will see the New Objects Only option, is ticked by default. This shows you what has been created, since the last snapshot. In here, you will gain insights into any new objects that shouldn’t have been created. In our example, we went back and forth between the MainPage and SecondPage. We should only see one new SecondPage at most between snapshots, but here we see that multiple were created and are still active.

multiple-secondpage

Stack Trace and Call Tree

Double click the SecondPage, and you will see all the new instances.

doubleclick-secondpage

You can click on Stack Trace, on the right, if you want to see the current stack trace to what created that instance.

stacktraceright

Alternatively, you can right click and press Show Call Tree. This will take you to the Allocation page, and place you on the exact instance, within the entire call tree.

calltree

Here we gain our first clue, that the instance is being created by the Clicked method on a button, in MainPage.

calltreedetail

Paths To Roots

Next, to find out what is referencing this instance, we look at Path To Roots. This is another button, in the tab on the right, in the snapshots tab.

pathstoroots

From here we can see it is being held by a list. However, that is as far as the profiler will take us. I speculate, that due to profiling a native app, it’s very hard to actually pin point the managed C# variable. Here, you must find out where that list is defined in your code. Hopefully at this point, you will have a reasonably good idea.

Warnings

  1. Always, rebuild your project before running the analyzer again. I have seen it not rebuild the project, and run an old version. This can cause quite a bit of confusion and/or wasted time.
  2. The Max Memory count is not accurate. Max memory keeps growing, even if app memory usage is not, actually growing. Since it is a max memory count, continuous spikes might cause it to continue to grow, even if the GC removes them soon after. This is where my label that shows the total memory used, is a more accurate representation of memory movement, though I will stress, not total memory usage.

Summary

Running through the steps again:

  1. Run Profiler
  2. Create Snapshots
  3. Monitor Differences between Snapshots
  4. View Stack Traces and Call Trees to see where instance is being created.
  5. View Path To Roots, to see where instance is being held.
  6. Educated Stab In Dark until you find where instance is being held.

Learn More

If you want to find out more, on how to pin point that memory leak, and potentially avoiding the educated stack in the dark, have a look at Tracking Memory Leaks In Xamarin With The Profiler – Part 2.


Posted

in

by

Tags: