FlowDocument Memory Issue in C#

泪湿孤枕 提交于 2019-11-30 13:40:56

If I've confirmed that there's a memory leak, here's what I would do to debug the problem.

  1. Install Debugging Tools for Windows from http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx#a
  2. Fire up Windbg from the installation directory.
  3. Launch your application and do the operations that leak memory.
  4. Attach Windbg to your application (F6).
  5. Type .loadby sos mscorwks
  6. Type !dumpheap -type FlowDocument
  7. Check the result of the above command. If you see multiple FlowDocuments, for each value of the first column (which contains the address), do

Type !gcroot <value of first column>

That should show you who's holding on to the reference.

Nitin Midha

We had a similar problem in which we were creating flow document in different thread, i noticed in memory profiler that objects were still there.

So far as i know, as described in this link

"When a FlowDocument is created, relatively expensive formatting context objects are also created for it in its StructuralCache. When you create multiple FlowDocs in a tight loop, a StructuralCache is created for each FlowDoc. Let's you called Gc.Collect at the end of the loop, hoping to recover some memory. StructuralCache has a finalizer releases this formatting context, but not immediately. The finalizer effectively schedules an operation to release contexts at DispatcherPriority.Background."

So until the dispatcher operations are completed, Flow document will be in memory. So the idea is to complete the dispatcher operations.

If you are in a thread in which Dispatcher is currently running then try code below, it will force all the operations in queue to be completed, as SystemIdle is the lowest priority:

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.SystemIdle, 
    new DispatcherOperationCallback(delegate { return null; }), null); 

If you are in a thread in which Dispatcher is not running, as in my case only single flow document was created in thread, so i tried something like:

var dispatcher = Dispatcher.CurrentDispatcher;
dispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
Dispatcher.Run();

this will queue a shut down at very end and then will run the dispatcher to clean the FlowDocument and then in end it will shut down the dispatcher.

FlowDocument uses System.Windows.Threading.Dispatcher to free all resources. It doesn't use finalizers because finalizers will block current thread until all resources will be free. So the user may see some UI freezings and so on. Dispatchers are running in background thread and have less impact on the UI.
So calling GC.WaitForPendingFinalizers(); has no influence on this. You just need to add some code to wait and allow Dispatchers to finish their work. Just try to add something like Thread.CurrentThread.Sleep(2000 /* 2 secs */);

EDIT: It seems to me that you found this problem during debugging of some application. I wrote the following very simple test case (console program):

    static void Main(string[] args)
    {
        Console.WriteLine("press enter to start");
        Console.ReadLine();

        var path = "c:\\1.rtf";

        for (var i = 0; i < 20; i++)
        {
            using (var stream = new FileStream(path, FileMode.Open))
            {
                var document = new FlowDocument();
                var range = new TextRange(document.ContentStart, document.ContentEnd);

                range.Load(stream, DataFormats.Rtf);
            }
        }

        Console.WriteLine("press enter to run GC.");
        Console.ReadLine();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        Console.WriteLine("GC has finished .");
        Console.ReadLine();
    }

I've tried to reproduce the problem. I run it several times and it was working perfectly - there were no leaks (about 3,2Kb all of the time and 36 handles). I couldn't reproduce it until I run this program in debug mode in the VS (just f5 instead of ctrl+f5). I received 20,5Kb at the beginning, 31,7Kb after loading and before GC and 31Kb after GC. It looks similar to your results.
So, could you please try to reproduce this problem running in release mode not under the VS?

I had previously done up #7. It was the first time I used Windbg and so I didn't know what to do with the address to find the references. Here is what I got.

 Address       MT     Size
0131c9c0 55cd21d8       84     
013479e0 55cd21d8       84     
044dabe0 55cd21d8       84     
total 3 objects
Statistics:
      MT    Count    TotalSize Class Name
55cd21d8        3          252 System.Windows.Documents.FlowDocument
Total 3 objects
0:011> !gcroot 0131c9c0
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 47c
Scan Thread 2 OSTHread be8
Scan Thread 4 OSTHread 498
DOMAIN(001657B0):HANDLE(WeakSh):911788:Root:0131ff98(System.EventHandler)->
0131fcd4(System.Windows.Documents.AdornerLayer)->
012fad68(MemoryTesting.Window2)->
0131c9c0(System.Windows.Documents.FlowDocument)
DOMAIN(001657B0):HANDLE(WeakSh):911cb0:Root:0131ca90(MS.Internal.PtsHost.PtsContext)->
0131cb14(MS.Internal.PtsHost.PtsContext+HandleIndex[])->
0133d668(MS.Internal.PtsHost.TextParagraph)->
0131c9c0(System.Windows.Documents.FlowDocument)
DOMAIN(001657B0):HANDLE(WeakSh):9124a8:Root:01320a2c(MS.Internal.PtsHost.FlowDocumentPage)->
0133d5d0(System.Windows.Documents.TextPointer)->
0131ca14(System.Windows.Documents.TextContainer)->
0131c9c0(System.Windows.Documents.FlowDocument)

(I put it in a code block for easier reading). This is after I closed the window. So it looks like it is being referenced by a few things. Now that I know this how do I go about freeing up these references so they can release the FlowDocument.

Thanks for your help. I feel like I am finally gaining some ground.

Make sure that the Parent of the FlowDocument isn't hanging around, see here. "Instantiating a FlowDocument automatically spawns a parent FlowDocumentPageViewer that hosts the content." If that control is hanging around it could be the source of your problem.

GC.Collect() by itself won't collect everything, you need to run:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Also, I've found the runtime doesn't always release collected memory right away, you should check the actual heap size instead of relying on task manager.

Consider releasing that file handle. Also consider using the "using" statement instead of calling IDisposable.Dispose (no pun intended).

I tried to reproduce your problem, but it doesn't happen on my machine.

Task Manager shows working set size, which isn't an accurate representation of a program's memory usage. Try using perfmon instead.

  1. Start -> Run -> perfmon.msc
  2. Add .NET CLR Memory/#Bytes in All Heaps for your application

Now repeat the experiment and check if that counter keeps going up. If it doesn't, it means you aren't leaking any managed memory.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!