Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one free memory used by heavy WPF Controls in a deterministic way?

I have been running into issues when Controls allocating big amounts of memory get destroyed and reconstructed using Input events from the UI.

Obviously, setting a Window's Content to null is not enough to free the memory the contained Control was using. It will be GCed eventually. But as Input events to destroy and construct Controls get more frequent, the GC seems to skip some objects.

I broke it down to this example:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseMove">
</Window>

C#:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i++ < 99999999; ) { } // just so we can follow visually in Process Explorer
                Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
            }
        }

        public MainWindow()
        {
            InitializeComponent();

            //Works 1:
            //while (true)
            //{
            //    window.Content = null;
            //    window.Content = new HeavyObject();
            //}
        }

        private void window_MouseMove(object sender, MouseEventArgs e)
        {
            if (window.Content == null)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                // Works 2:
                //new HeavyObject();
                //window.Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
                //return;
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = null;
            }
        }
    }
}

This does allocate a ~750MB object (HeavyObject) at each MouseMove event and put it as the main Window's Content. If the Cursor is kept still on the Window the Content change will endlessly re-trigger the event. The Reference to HeavyObject is nulled before its next construction and a futile attempt to GC its remains is triggered.

What we can see in ProcessExplorer / Taskman is, that there is sometimes 1.5GB or more allocated. If not, move the mouse wildly and leave and re-enter the window. If you compile for x86 without LARGEADDRESSAWARE, you'll get an OutOfMemoryException instead (limit ~1.3GB).

You won't encounter this behavior if you either allocate the HeavyObject in an endless loop without using mouse input (uncomment section "Works 1") or if you trigger the allocation by mouse input but do not put the object into the visual tree (uncomment seciton "Works 2").

So i suppose it has something to do with the visual tree lazily freeing resources. But then there is another strange effect: If 1.5GB are consumed during the cursor is outside the Window, it seems the GC won't kick in until MouseMove is triggered again. At least, memory consumption seems to stabilize as long as no further events are triggered. So either there is a long-living reference left somewhere or GC gets lazy when there is no activity.

Seems strange enough to me. Can you figure out what's going on?

Edit: As BalamBalam commented: The data behind the Control could be dereferenced before destroying the Control. That would have been plan B. Nevertheless maybe there is a more generic solution.

Saying such Controls are bad code is not helpful. I would like to know why an object I hold no reference to gets GCed if I leave the UI alone for a few Ticks after dereferencing it, but lingers around forever if another one (that has to have been created by user input) immediately takes its place.

like image 662
die_hoernse Avatar asked Jan 24 '12 14:01

die_hoernse


1 Answers

Ok, I think I might know whats going on here. Take it with a pinch of salt though ...

I modified your code slightly.

Xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseDown="window_MouseDown">
</Window>

Code Behind

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i < 750000000; i++ )
                {                    
                    unchecked
                    {
                        x[i] = (byte)i;       
                    }
                }

                // Release optimisation will not compile the array
                // unless you initialize and use it. 
                Content = "Loadss a memory!! " + x[1] + x[1000]; 
            }
        }

        const string msg = " ... nulled the HeavyObject ...";

        public MainWindow()
        {
            InitializeComponent();
            window.Content = msg;
        }

        private void window_MouseDown(object sender, MouseEventArgs e)
        {
            if (window.Content.Equals(msg))
            {
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = msg;
            }
        }
    }
}

Now run this up and click on the window. I modified the code to not use GC.Collect() and to set the memory on mousedown. I am compiling in Release mode and running without a debugger attached.

Click slowly on the window.

The first click you see the memory usage go up by 750MBytes. Subsequent clicks toggle the message between .. Nulled the heavy object.. and Loads a memory! so long as you click slowly. There is a slight lag as the memory is allocated and deallocated. The memory usage shouldn't go over 750MBytes but it also doesn't decrease when the heavyobject is nulled. This is by-design as the GC Large Object Heap is lazy and collects large object memory when new large object memory is needed. From MSDN:

If I don't have enough free space to accommodate the large object allocation requests, I will first attempt to acquire more segments from the OS. If that fails, then I will trigger a generation 2 garbage collection in hope of freeing up some space.

Click fast on the window

Around 20 clicks. What happens? On my PC memory usage does not go up but the mainwindow now does not update the message any more. It freezes. I dont get an out of memory exception and the overall memory usage of my system stays flat.

Stress the system using MouseMove on the Window

Now replace the MouseDown for a MouseMove event (as per your question code) by changing this Xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseDown">
</Window>

I can move the mouse over it for quite a while but eventually it throws an OutofMemoryException. A good way to force the exception is do multiple moves then try to maximise the window.

The Theory

Ok so this is my theory. As your UI thread is running flat out creating and deleting memory inside a message loop event (MouseMove), the Garbage collector is unable to keep up. Garbage collection on the Large Object Heap is done when memory is needed. It is also an expensive task so performed as infrequently as possible.

The lag we noticed when fast clicking on/off/on/off becomes far more pronounced when performed in a MouseMove. If another large object is to be allocated, according to that MSDN article the GC will either try to collect (if memory is not available) or will start using disk.

System Is in Low Memory Situation: This happens when I receive the high memory notification from the OS. If I think doing a generation 2 GC will be productive, I will trigger one.

Now this part is interesting and I'm guessing here but

Finally, as of right now, the LOH is not compacted as a part of collection, but that is an implementation detail that should not be relied on. So to make sure something is not moved by the GC, always pin it. Now take your newfound LOH knowledge and go take control of the heap.

So, if the Large Object Heap is not compacted, and you request multiple large objects in a tight loop, is it possible that collections lead to fragmentation eventually causing an OutOfMemoryException even though there should theoretically be enough memory?

The solution

Let's free up the UI thread a bit to allow the GC room to breathe. Wrap the allocation code in an asynchronous Dispatcher call and use a low priority, such as SystemIdle. This way it will only allocate the memory when the UI thread is free.

    private void window_MouseDown(object sender, MouseEventArgs e)
    {
        Dispatcher.BeginInvoke((ThreadStart)delegate()
            {
                if (window.Content.Equals(msg))
                {
                    window.Content = new HeavyObject();
                }
                else
                {
                    window.Content = msg;
                }
            }, DispatcherPriority.ApplicationIdle);
    }

Using this I can dance the mouse pointer all over the form and it never throws an OutOfMemoryException. Whether its really working or has "fixed" the problem I don't know but its worth testing.

In conclusion

  • Note that objects of this size are going to be allocated on the Large Object Heap
  • LOH allocations trigger a GC if not enough space is available
  • The LOH is not compacted during a GC cycle so fragmentation may occur, leading to OutOfMemoryException despite there theoretically being enough memory
  • Ideally re-use large objects and don't reallocate. If that is not possible:
    • Allcate your large objects in a background thread to decouple from the UI thread
    • To allow the GC room to breathe, use the Dispatcher to decouple the allocation until the application is idle
like image 55
Dr. Andrew Burnett-Thompson Avatar answered Nov 08 '22 00:11

Dr. Andrew Burnett-Thompson