ratishphilip/CompositionProToolkit

Memory Leak issue with ImageFrame

Closed this issue · 16 comments

Hi @ratishphilip

I have noticed memory leak issue when using ImageFrame. When ImageFrame is used, and I navigate away from the page, to a new page, the GC doesn't seem to collect the previous page. As a result, the memory keeps on increasing. Here is how to simulate:

  1. I have a GridView with around 30 items bound via ViewModel. FYI, this issue is not related to x:Bind memory leak, as I have coded to fix the x:Bind Memory Issue.
  2. In the complex ItemTemplate, I am displaying image as below (this is how I am displaying image - which doesn't result in memory leak).

<Rectangle RadiusX="8" RadiusY="8" Opacity="0.8" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" > <Rectangle.Fill> <ImageBrush Stretch="UniformToFill" fwExtensions:ImageExtensions.CachedSource="{x:Bind PosterImageLow}" /> </Rectangle.Fill> </Rectangle>

  1. When the app starts, it consumes around 106 MB of memory.
  2. On navigation to an empty page, the memory drops by a few MBs, and then when navigarted back to this start page, the memory comes back to 106MB, showing that the previous page was garbage collected.
  3. The performance is also superb. No matter how many times, I navigate to empty and back to the page, the memory stays at 106.

Now, just replace the above image with ImageFrame as below, and on first start, it consumes around 200MB of memory. When navigated to an empty page, the memory comes down to around 190 MB. When again navigated to the first page, the memory jumps to 270 MB. Navigate to empty page and back again, it goes to 344MB. One more time, 418MB. Another one, 491 and it keeps going.

And app becomes very slow after one or two iterations.

<cmpToolkit:ImageFrame x:Name="imgPoster" RenderFast="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="#AA232323" FrameBackground="#AA232323" BorderThickness="0" Stretch="UniformToFill" Source="{x:Bind BackgroundImageLow}" PlaceholderBackground="#AA232323" TransitionMode="FadeIn" ShowPlaceholder="True" />

Thanks for reporting this @naweed. I will look into this issue.

Just a suggestion. I am no expert in composition APIs, but I think the composition canvas or surface needs to be removed from VisualTree on Unload event of the control, so that the page can release it.

Hi @naweed
I have done some changes in ImageFrame to address this issue and need your help to test it.

Could you please download the NuGet package available here and use it in your application?

You have to add the following changes too
ImageFrame now implements IDisposable so now it has a .Dispose() method which must be called during the OnNavigatedFrom() override of your page. This will dispose the surfaces used internally.

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    foreach (var imgFrame in myGridView.GetDescendantsOfType<ImageFrame>())
    {
        imgFrame.Dispose();
    }

    VisualTreeHelper.DisconnectChildrenRecursive(myGridView);
    myGridView.ItemsSource = null;
}

You have to replace the myGridView with the name of your GridView.

Could you try it and let me know if the memory is being released and whether the ObjectDisposedEvent still occurs?

Will try it tomorrow and get back to you.

Thanks.

Tried, and can confirm that this issue is resolved. I did however had to add this code:

`
public static class VisualTreeHelperExtensions
{
public static T GetFirstDescendantOfType(this DependencyObject start) where T : DependencyObject
{
return start.GetDescendantsOfType().FirstOrDefault();
}

    public static IEnumerable<T> GetDescendantsOfType<T>(this DependencyObject start) where T : DependencyObject
    {
        return start.GetDescendants().OfType<T>();
    }

    public static IEnumerable<DependencyObject> GetDescendants(this DependencyObject start)
    {
        var queue = new Queue<DependencyObject>();
        var count = VisualTreeHelper.GetChildrenCount(start);

        for (int i = 0; i < count; i++)
        {
            var child = VisualTreeHelper.GetChild(start, i);
            yield return child;
            queue.Enqueue(child);
        }

        while (queue.Count > 0)
        {
            var parent = queue.Dequeue();
            var count2 = VisualTreeHelper.GetChildrenCount(parent);

            for (int i = 0; i < count2; i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                yield return child;
                queue.Enqueue(child);
            }
        }
    }

    public static T GetFirstAncestorOfType<T>(this DependencyObject start) where T : DependencyObject
    {
        return start.GetAncestorsOfType<T>().FirstOrDefault();
    }

    public static IEnumerable<T> GetAncestorsOfType<T>(this DependencyObject start) where T : DependencyObject
    {
        return start.GetAncestors().OfType<T>();
    }

    public static IEnumerable<DependencyObject> GetAncestors(this DependencyObject start)
    {
        var parent = VisualTreeHelper.GetParent(start);

        while (parent != null)
        {
            yield return parent;
            parent = VisualTreeHelper.GetParent(parent);
        }
    }

    public static bool IsInVisualTree(this DependencyObject dob)
    {
        return Window.Current.Content != null && dob.GetAncestors().Contains(Window.Current.Content);
    }

    public static Rect GetBoundingRect(this FrameworkElement dob, FrameworkElement relativeTo = null)
    {
        if (relativeTo == null)
        {
            relativeTo = Window.Current.Content as FrameworkElement;
        }

        if (relativeTo == null)
        {
            throw new InvalidOperationException("Element not in visual tree.");
        }

        if (dob == relativeTo)
            return new Rect(0, 0, relativeTo.ActualWidth, relativeTo.ActualHeight);

        var ancestors = dob.GetAncestors().ToArray();

        if (!ancestors.Contains(relativeTo))
        {
            throw new InvalidOperationException("Element not in visual tree.");
        }

        var pos =
        dob
        .TransformToVisual(relativeTo)
        .TransformPoint(new Point());
        var pos2 =
        dob
        .TransformToVisual(relativeTo)
        .TransformPoint(
        new Point(
        dob.ActualWidth,
        dob.ActualHeight));

        return new Rect(pos, pos2);
    }
}

`

Great.
Hey, you need not add the VisualTreeHelperExtensions code. It is already added in CompositionProToolkit.

Just add using CompositionProToolkit; to your code.

Are you facing anymore ObjectDisposedExceptions now?

Nope. Seems to be resolved. Might have been issue with the memory usage, but I am not 100% sure. The device lost event shouldn't be linked to memory usage,

No, I am not linking Device Lost event to memory usage.
Just wanted to know if you were facing any exceptions separately.
Also, could you tell me what kind of Panel have you specified for your GridView?

I am using a custom grid which auto resized the elements to fill the entire width of the screen.

But I also tried with GridView with ItemsGridWrapPanel, and got the same issue.

What I mean to say is that you should use a Panel which supports virtualization. This will help reduce the memory usage. Does your custom grid support virtualization?

I don't think so.

Even though, between using the ImageFrame and regular image in the same panel, the memory difference is huge.

But no worries, I will see how best I can optimize my code.

Thank you for all your great help.

You are welcome.

The latest version of CompositionProToolkit is now available in Nuget.