Alois Kraus

blog

  Home  |   Contact  |   Syndication    |   Login
  133 Posts | 8 Stories | 368 Comments | 162 Trackbacks

News



Archives

Post Categories

Programming

If you have ever tried to create a WPF application which has a larger memory footprint (>500MB) you will notice some random hangs in the UI which become several second hangs for no apparent reason. On a test machine with a decent Xeon CPU I have seen 35s UI hangs because WPF frequently (up to every 850ms) calls GC.Collect(2). The root cause of the problem is that WPF was designed with a business application developer in mind who never gets resource management right. For that reason WPF Bitmaps and other things do not even care to implement the IDisposable interface to clean up resources deterministically. Instead the cleanup is left as an exercise for the Garbage Collector with the Finalizer thread working hand in hand.

That can lead to problems. Suppose a 32 bit application where the user is scrolling through a virtual ListView with many bitmaps inside it. This operation will cause the allocation of many temporary Bitmaps which will quickly become garbage. Because the Bitmaps are small objects on the managed heap but the actual Bitmap data is stored in unmanaged memory the Garbage Collector sees no need to clean things up for a long time. In effect it did happen that your application ran of unmanaged memory long before the Garbage Collector was able to release the bitmaps in the Finalizer. That lead to one of the worst hacks in WPF. It is called MemoryPressure. Lets have a look how it is implemented:

http://referencesource.microsoft.com/#PresentationCore/Core/CSharp/MS/Internal/MemoryPressure.cs
//
// About the thresholds:  
// For the inter-allocation threshold 850ms is the longest time between allocations on a high-end
// machine for an image application loading many large (several M pixel) images continuously.
// This falls well below user-interaction time (which is on the order of several seconds) so it
// differentiates nicely between the two
//
// The initial threshold of 1MB is so we don't force GCs when the total amount of unmanaged memory
// isn't a big deal.  The point of this code is to stop unmanaged memory from spiraling out of control
// at that point it's typically in the 10s of MBs.  This threshold thus could potentially be increased
// but current testing shows it is adequate.
//
// The max time between collections was set to 30 sec because that is a 'long time' - this is
// for the case where allocations (and frees) of images are happening continously without 
// pause - we haven't seen scenarios that do this yet so it's possible this threshold could also 
// be increased
// 
private const int INITIAL_THRESHOLD = 0x100000;          // 1 MB initial threshold
private const int INTER_ALLOCATION_THRESHOLD = 850;     // ms allowed between allocations
private const int MAX_TIME_BETWEEN_COLLECTIONS = 30000; // ms between collections
/// <summary>
/// Check the timers and decide if enough time has elapsed to
/// force a collection
/// </summary>
private static void ProcessAdd()
{
    bool shouldCollect = false;

    if (_totalMemory >= INITIAL_THRESHOLD)
    {
        //
        // need to synchronize access to the timers, both for the integrity
        // of the elapsed time and to ensure they are reset and started
        // properly
        //
        lock (lockObj)
        {
            //
            // if it's been long enough since the last allocation
            // or too long since the last forced collection, collect
            //
            if (_allocationTimer.ElapsedMilliseconds >= INTER_ALLOCATION_THRESHOLD
                || (_collectionTimer.ElapsedMilliseconds > MAX_TIME_BETWEEN_COLLECTIONS))
            {
                _collectionTimer.Reset();
                _collectionTimer.Start();
            
                shouldCollect = true;
            }
            _allocationTimer.Reset();
            _allocationTimer.Start();
        }

        //
        // now that we're out of the lock do the collection
        //
        if (shouldCollect)
        {
            Collect();
        }
    }

    return;
}

/// <summary>
/// Forces a collection.
/// </summary>
private static void Collect()
{
    //
    // for now only force Gen 2 GCs to ensure we clean up memory
    // These will be forced infrequently and the memory we're tracking
    // is very long lived so it's ok
    //
    GC.Collect(2);
}

This beauty is calling GC.Collect(2) every 850ms if in between no Bitmap was allocated or every 30s regardless of how many Bitmaps were allocated. With .NET 4.5 we have got concurrent garbage collection which dramatically reduces blocking all application threads while a garbage collection is happening. For common application workloads a "normal" .NET application gets 10-15% faster by doing no change to the code. These improvements are all nullified by calling a forceful full blocking garbage collection.

To demonstrate the effect I have created a simple test application which allocates on a background thread 1 GB of small objects while on the UI thread we allocate one WPF bitmap every 850ms and compare that to allocating an "old" WinForms Bitmap object also every 850ms.

// WPF allocation                 
BitmapFrame img = null;
while (true)
{
    myBitmapStream2.Position = 0;
    img = BitmapFrame.Create(myBitmapStream2); // WPF Bitmaps have no Dispose method!
    Thread.Sleep(850);
    if (backgroundAllocation.IsCompleted)
    {
        break;
    }
}

// Winforms Bitmap allocation 
Bitmap img = null;
while (true)
{
    myBitmapStream2.Position = 0;
    img = new Bitmap(myBitmapStream2); 
    Thread.Sleep(850);
    if (backgroundAllocation.IsCompleted)
    {
        break;
    }
    img.Dispose();
}
 

If you measure for some heap sizes you quickly see that your application will become dramatically slower the more memory it consumes due to the forced blocking garbage collection caused by WPF. The x-axis shows the managed heap size in MB and the y-axis shows the time needed to allocate 1 GB of small objects in a background thread.

image

You have a multi GB WPF application and the user experience is just awful and slow? You can google for good answers on Stackoverflow

which tell you that you need to use Reflection to set private fields in the internal MemoryPressure class of WPF. Not exactly a production grade "fix" to the issue.

But there is hope. The new public beta of .NET Framework 4.6.2 contains a fix for it. The MemoryPressure class is gone and your Stackoverflow "fix" will cause exceptions if you did not prepare for the impossible that Microsoft did dare to remove internal classes. WPF now adheres to the long time recommended GC.AddMemoryPressure call to tell the Garbage Collector that some managed objects also consume significant unmanaged memory.

// System.Windows.Media.SafeMILHandleMemoryPressure
internal SafeMILHandleMemoryPressure(long gcPressure)
{
    this._gcPressure = gcPressure;
    this._refCount = 0;
    if (this._gcPressure > 8192L)
    {
        MemoryPressure.Add(this._gcPressure);
        return;
    }
    GC.AddMemoryPressure(this._gcPressure);
}

With .NET 4.6.2 you finally get the possibility back to create snappy managed applications without long forced garbage collection pauses. You can measure the GC pause times with my custom WPA profile in no time:

image

That is nice but you can see with my custom WPA profile and the streamlined default.stacktags file even more:

image

There you can clearly see that while the managed heap grows the Induced GC times get bigger just as you would expect from the GC regions. To get the same view you need to download my simplified WPA profile which I have updated with the latest stacktags I found useful during past analysis. To make that active you need to open from the menu Trace - Trace Properties and remove current file and add the downloaded stacktags file. Or you can also simply overwrite the default.stacktags file that comes with WPA.

image

The new improved stacktags file gives you fast insights into your application and your system which is not really possible with other tools. With a nice stacktags file you can create your very own view of the system. The updated stacktags file contains tags for common serializers, exception processing, and many more things which are useful during analysis of performance issues or application failures.

posted on Thursday, April 14, 2016 9:27 AM

Feedback

# re: .NET 4.6.2 Makes WPF For Big Apps Fun Again 4/14/2016 10:46 PM Jonathon Rossi
I remember running into this problem with a large WPF app in 2010 and having to hack around it. Still remember the problems with BitmapSourceSafeMILHandle. Microsoft Support at the time closed my support ticket as "by design", glad to see they are finally doing something about it even if I haven't touched WPF in years, I wonder if the Visual Studio team requested the fix. Thanks for the great article.

# re: .NET 4.6.2 Makes WPF For Big Apps Fun Again 8/7/2017 2:19 AM brunooo
Danke Alois für deinen Blog!

Aktuell bin ich damit beschäftigt einen Rendering Server zu implementieren der Kartendaten on-the-fly rendered. Lastentests zeigen mir, dass eine Menge an nativem Memory allokiert wird (Modul: wpfgfx_v4000.dll) obwohl ich nur managed WPF Klassen verwende. Habe nachdem ich deinen Eintrag gelesen habe mal auf 4.7 als Zielplattform umgestellt - bislang sieht es gut aus! Wie auch immer - vielen Dank schon mal für deinen Input!

Post A Comment
Title:
Name:
Email:
Comment:
Verification: