ReactiveUI bindings seem to prevent garbage collection from occurring
Asked Answered
M

1

11

We are currently using ReactiveUI to help build a fairly large WPF based Windows application. All was going well, until we discovered that our application was consuming huge amounts of memory ... basically all of our views, view-models and models were not being garbage collected.

Based on the information from memory profilers, such as Jet Brains dotMemory, ReactiveUI appears to be the primary culprit. In particular are the The ReactiveUI bindings that we are configuring in our views, even though we are using best practices and ensuring that all bindings are disposed when the view is deactivated.

The following is a sample of one of the views that we are creating. Any thoughts as to where we could be going wrong would be greatly appreciated.

public partial class RunbookInputsView : IViewFor<RunbookInputsViewModel>
{
    public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
        "ViewModel", typeof(RunbookInputsViewModel), typeof(RunbookInputsView));

    public RunbookInputsView()
    {
        InitializeComponent();

        this.WhenActivated(d =>
        {
            d(this.OneWayBind(ViewModel, vm => vm.AddInput, v => v.AddInput.Command));                
            d(this.OneWayBind(ViewModel, vm => vm.Inputs, v => v.Inputs.ItemsSource));
        });
    }

    object IViewFor.ViewModel
    {
        get { return ViewModel; }
        set { ViewModel = (RunbookInputsViewModel)value; }
    }

    public RunbookInputsViewModel ViewModel
    {
        get { return (RunbookInputsViewModel) GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }
}
Menell answered 20/8, 2015 at 18:54 Comment(4)
Have you confirmed that deactivation logic is executing? Try adding d(Disposable.Create(() => System.Diagnostics.Debug.WriteLine("DEACTIVATED"))) to one of your WhenActivated callsDelirious
Using breakpoints it appears that the reactive bindings are being disposed of correctly.Menell
My investigation is now focusing on the use of CreateDerivedCollection and the lambda that is used to select new items for the derived collection. It seems that I have memory issues when the lambda is referencing members of my class. The following seems ok: source.CreateDerivedCollection(x => ViewModel(x)); But this seems to have problems: source.CreateDerivedCollection(x => ViewModel(x, this.Something));Menell
I think I finally tracked down the leak to the ReactiveUI ViewModelViewHost class, specifically situations where you have user controls containing a ViewModelViewHost where the user control is not displayed (like when hidden within an expander). Removed the ViewModelViewHost and replaced it with DataTemplate/ContentControl and the everything started to be GC'd correctly.Menell
S
2

From the question it's hard to tell where the leak is coming from. Let the leak happen for a while, then attach to the process with windbg (part of the Debugging Tools For Windows) (Note: you may need to build x86 or x64 for this to work.)

Once you've attached, set up for .net debugging by entering the commands:

.symfix
sxe clr
sxd av
.loadby sos clr

You can then use !dumpheap -stat to get the memory usage of each type. This produces output in the following format: (I truncated the class names, and the list for readability.)

0:012> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
000007fefa55d2e8        1           24 System.[...]TransportSinkProvider
000007fefa55ce08        1           24 System.Run[...]rtSinkProvider
000007fee7c32df0        1           24 System.LocalDataStoreHolder
000007fee7c2ff78        1           24 System.Colle[...]
000007fee7c2ece0        1           24 System.Resources.FastResourceComparer
000007fee7c2ead0        1           24 System.Resources.ManifestBasedResourceGroveler
000007fee7c2ea70        1           24 System.[...]eManagerMediator
000007fee4cc1b70        4         1216 System.Xml.XmlDocument

If you have a memory leak this is where you'll see the leaked objects. (There should be a lot of them.) Once you've identified what is leaking, you can then do a !dumpheap -type to get a list of the actual objects. (For this example I'll use System.Xml.XmlDocument. The type name is case sensitive, and must be fully qualified.)

0:012> !dumpheap -type System.Xml.XmlDocument
         Address               MT     Size
0000000002af9050 000007fee4cc1b70      304     
0000000002afa628 000007fee4cc1b70      304     
0000000002b0ea30 000007fee4cc1b70      304     
00000000037e2780 000007fee4cc1b70      304     

Statistics:
              MT    Count    TotalSize Class Name
000007fee4cc1b70        4         1216 System.Xml.XmlDocument

Your list will probably be a lot larger, but probability says that any random instance of the leaked type will be something you're interested in. If we do a !do on one of those addresses, we'll get output that looks like this:

0:012> !do 2af9050
Name:        System.Xml.XmlDocument
MethodTable: 000007fee4cc1b70
EEClass:     000007fee4ae7f00
Size:        304(0x130) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee4cc2b40  40004fc        8   System.Xml.XmlNode  0 instance 0000000000000000 parentNode
000007fee4cc2258  400050a       10 ...XmlImplementation  0 instance 0000000002af9180 implementation
000007fee4cc22f0  400050b       18 ....Xml.DomNameTable  0 instance 0000000002af92e0 domNameTable
[Entries removed for clarity]
000007fee4cc26f0  400052f      108 ...m.Xml.XmlResolver  0 instance 0000000000000000 resolver
000007fee7c18c48  4000530      126       System.Boolean  1 instance                0 bSetResolver
000007fee7c113e8  4000531      110        System.Object  0 instance 0000000002af9788 objLock
000007fee4cc11b0  4000532      118 ....Xml.XmlAttribute  0 instance 0000000000000000 namespaceXml

You can use !do with any of the objects listed in the table for more information. Types like System.String and System.Boolean will spit out their actual values. If it isn't clear from the object where it was created, the next step would likely be to use !gcroot -nostacks to find the references to our objects.

0:012> !gcroot -nostacks 2af9050
HandleTable:
    00000000006117d8 (pinned handle)
    -> 0000000012a55748 System.Object[]
    -> 0000000002af9050 System.Xml.XmlDocument

Found 1 unique roots (run '!GCRoot -all' to see all roots).

There's quite a handful more commands, and this is already too long. The !help command provides a nice listing. (To use any of them, you'd need to prefix the command with !. !help [command] gives detailed information about a specific command. For example !help dumpobj:

0:012> !help dumpobj
-------------------------------------------------------------------------------
!DumpObj [-nofields] <object address>

This command allows you to examine the fields of an object, as well as learn 
important properties of the object such as the EEClass, the MethodTable, and 
the size.

You might find an object pointer by running !DumpStackObjects and choosing
from the resultant list. Here is a simple object:

    0:000> !DumpObj a79d40
    Name: Customer
    MethodTable: 009038ec
    EEClass: 03ee1b84
    Size: 20(0x14) bytes
     (C:\pub\unittest.exe)
    Fields:
          MT    Field   Offset                 Type  VT     Attr    Value Name
    009038ec  4000008        4             Customer   0 instance 00a79ce4 name
    009038ec  4000009        8                 Bank   0 instance 00a79d2c bank

Note that fields of type Customer and Bank are themselves objects, and you can 
run !DumpObj on them too. You could look at the field directly in memory using
the offset given. "dd a79d40+8 l1" would allow you to look at the bank field 
directly. Be careful about using this to set memory breakpoints, since objects
can move around in the garbage collected heap.

What else can you do with an object? You might run !GCRoot, to determine what 
roots are keeping it alive. Or you can find all objects of that type with 
"!DumpHeap -type Customer".

The column VT contains the value 1 if the field is a valuetype structure, and
0 if the field contains a pointer to another object. For valuetypes, you can 
take the MethodTable pointer in the MT column, and the Value and pass them to 
the command !DumpVC.

The abbreviation !do can be used for brevity.

The arguments in detail:
-nofields:     do not print fields of the object, useful for objects like 
                  String
Socialistic answered 1/9, 2015 at 16:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.