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); }
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With