Thursday, 29 April 2010

UserControls aren’t (garbage) collected also if you detach the event handlers.

Working with Silverlight 3 I noticed a strange thing about UserControl not garbage collected.

Look at this code:


namespace TestMemoria
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();

TestObject = new Controllo();

//Add the UserControl to the LayoutRoot
this.LayoutRoot.Children.Add(TestObject);
Canvas.SetLeft(TestObject, 100);
Canvas.SetTop(TestObject, 100);

// Listen to the MouseEnter Event
TestObject.BtnBack.Click += new RoutedEventHandler(BackToMainPage);
}

private void BackToMainPage(object sender, RoutedEventArgs args)
{
// Detach the Event
TestObject.BtnBack.Click -= new RoutedEventHandler(BackToMainPage);

// I remove the UserControl from the stage
this.LayoutRoot.Children.Remove(TestObject);

// set to null
TestObject = null;
}

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
GC.Collect();
}

Controllo TestObject;
}
}



This is the code behind of a very simple Silverlight UserControl with another UserControl(of type Controllo) dynamically added in the constructor code.

Controllo has got a button whose click event is handled by the BackToMainPage method.

Inside this method I detach the event handler , remove the Controllo object from the LayoutRoot and set the reference TestObject to null.

Then i used windbg and i typed the initial commands:


0:015> .load C:\\Program Files\\Microsoft Silverlight\\3.0.50106.0\\sos.dll
0:015> .load C:\\MyDlls\\sosex.dll
0:015> .sympath srv*;C:\Projects\TestMemoria2\TestMemoria\Bin\Debug
Symbol search path is: srv*;C:\Projects\TestMemoria2\TestMemoria\Bin\Debug
Expanded Symbol search path is: cache*C:\Program Files\Debugging Tools for Windows (x86)\sym;SRV*http://msdl.microsoft.com/download/symbols;c:\projects\testmemoria2\testmemoria\bin\debug
0:015> .reload
Reloading current modules
................................................................
..................................



with these commands i loaded the sos and sosex dlls and loaded the microsoft public
symbols and the symbols of the application.

Then i tryed to understand if the Controllo object had been collected or not after the button
was clicked.

So i ran the application and clicked the Button of Controllo to remove the Controllo object from the stage having Windbg attached to the process.

After the click i issued these commands to check handles for the Controllo object:


0:027> !dumpheap -stat -type TestMemoria
total 3 objects
Statistics:
MT Count TotalSize Class Name
05283894 1 44 TestMemoria.App
05283dc4 1 84 TestMemoria.Controllo
05283c60 1 84 TestMemoria.MainPage
Total 3 objects
0:027> !dumpheap -mt 05283dc4
Address MT Size
0885cb34 05283dc4 84
total 1 objects
Statistics:
MT Count TotalSize Class Name
05283dc4 1 84 TestMemoria.Controllo
Total 1 objects
0:027> !gcroot 0885cb34
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 4 OSTHread 1674
Scan Thread 24 OSTHread 18e0
Scan Thread 25 OSTHread 1e08
DOMAIN(06BE9ED8):HANDLE(Pinned):52912f8:Root: 09824260(System.Object[])->
08825650(System.Collections.Generic.List`1[[System.Object, mscorlib]])->
08865960(System.Object[])->
0885cb34(TestMemoria.Controllo)->


As you can see (ouptut of the !gcroot command) there are still handles to the 'Controllo' object.

On this blog: http://msmvps.com/blogs/senthil/archive/2008/05/29/the-case-of-the-leaking-thread-handles.aspx you can read
The main root was (System.Object[]), which indicated that the next object in the object graph (Hashtable) was a static member of some class.
So i started to search for a static variable of type System.Collections.Generic.List`1[[System.Object, mscorlib]].

I thought that a good starting point could be to examine the stack situation
when the framework calls the BackToMainPage method in response to the Button Click.

So i put a breakpoint a the start of the method, reached that breakpoint (maybe after restarting the debugging session) and examined the stack
with these commands:


0:026> !bpmd TestMemoria TestMemoria.MainPage.BackToMainPage
Found 1 methods...
MethodDesc = 04d53c24
Adding pending breakpoints...
0:026> g
(1b5c.185c): CLR notification exception - code e0444143 (first chance)
JITTED TestMemoria!TestMemoria.MainPage.BackToMainPage(System.Object, System.Windows.RoutedEventArgs)
Setting breakpoint: bp 04F005A0 [TestMemoria.MainPage.BackToMainPage(System.Object, System.Windows.RoutedEventArgs)]
Breakpoint: JIT notification received for method TestMemoria.MainPage.BackToMainPage(System.Object, System.Windows.RoutedEventArgs).
Breakpoint 0 hit
eax=04d53c24 ebx=00000000 ecx=079dd9ec edx=079fd528 esi=03cf18c0 edi=02afd4ec
eip=04f005a0 esp=02afd420 ebp=02afd438 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00200202
04f005a0 55 push ebp

0:005> !clrstack -a
OS Thread Id: 0x185c (5)
ESP EIP
02afd420 04f005a0 TestMemoria.MainPage.BackToMainPage(System.Object, System.Windows.RoutedEventArgs)
PARAMETERS:
this () = 0x079dd9ec
sender () = 0x079fd528
args (0x02afd424) = 0x07a06618

02afd428 055250ed System.Windows.Controls.Primitives.ButtonBase.OnClick()
PARAMETERS:
this (0x02afd434) = 0x079fd528
LOCALS:
0x02afd430 = 0x079fefe0
0x02afd42c = 0x07a06618

02afd440 05524fef System.Windows.Controls.Button.OnClick()
PARAMETERS:
this =
LOCALS:


02afd450 05524f1d System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs)
PARAMETERS:
this (0x02afd454) = 0x079fd528
e (0x02afd450) = 0x07a065b0

02afd460 05524e81 System.Windows.Controls.Control.OnMouseLeftButtonUp(System.Windows.Controls.Control, System.EventArgs)
PARAMETERS:
ctrl =
e =

02afd470 04f939cd MS.Internal.JoltHelper.FireEvent(IntPtr, IntPtr, Int32, System.String)
PARAMETERS:
unmanagedObj (0x02afd4d8) = 0x03cf18c0
unmanagedObjArgs (0x02afd4d4) = 0x06511e60
argsTypeIndex (0x02afd508) = 0x000000a9
eventName (0x02afd504) = 0x07a06594
LOCALS:
0x02afd4c8 = 0x07a065b0
0x02afd4c4 = 0x079fd528
0x02afd4c0 = 0x00000000
0x02afd4bc = 0x00000000
0x02afd4b8 = 0x00000000
0x02afd4b4 = 0x00000000
0x02afd4b0 = 0x079fd528
0x02afd4ac = 0x07a065e0
0x02afd4d0 = 0x00000004
0x02afd4a8 = 0x00000000
0x02afd4a4 = 0x00000000
0x02afd4a0 = 0x00000000
0x02afd49c = 0x00000000
0x02afd498 = 0x00000000
0x02afd4cc = 0x00000000

02afd654 51a317b0 [GCFrame: 02afd654]
02afd710 51a317b0 [ContextTransitionFrame: 02afd710]
02afd808 51a317b0 [UMThkCallFrame: 02afd808]



You can see that the static Method MS.Internal.JoltHelper.FireEvent is involved.
So i searched for static variables defined in the MS.Internal.JoltHelper class.


0:005> !name2ee System.Windows.dll MS.Internal.JoltHelper
Module: 04dce734
Assembly: System.Windows.dll
Token: 020001f7
MethodTable: 04e01cac
EEClass: 04e70c90
Name: MS.Internal.JoltHelper
0:005> !dumpclass 04e70c90
Class Name: MS.Internal.JoltHelper
mdToken: 020001f7
File: c:\Program Files\Microsoft Silverlight\3.0.50106.0\System.Windows.dll
Parent Class: 04be73ec
Module: 04dce734
Method Table: 04e01cac
Vtable Slots: 4
Total Method Slots: 1e
Class Attributes: 180 Abstract,
NumInstanceFields: 0
NumStaticFields: b
MT Field Offset Type VT Attr Value Name
04e07bec 400067e a0c ...Int32, mscorlib]] 0 shared static EventSenderOverrides
>> Domain:Value 063ac6f0:NotInit 063aff48:079c5274 <<>> Domain:Value 063ac6f0:NotInit 063aff48:079c5620 <<>> Domain:Value 063ac6f0:NotInit 063aff48:079c5650 <<>> Domain:Value 063ac6f0:NotInit 063aff48:500 <<>> Domain:Value 063ac6f0:NotInit 063aff48:0 <<>> Domain:Value 063ac6f0:NotInit 063aff48:03cb03c0 <<>> Domain:Value 063ac6f0:NotInit 063aff48:1 <<>> Domain:Value 063ac6f0:NotInit 063aff48:0 <<>> Domain:Value 063ac6f0:NotInit 063aff48:0 <<>> Domain:Value 063ac6f0:NotInit 063aff48:079c5690 <<>> Domain:Value 063ac6f0:NotInit 063aff48:079c5678 <<> !do 079c5650
Name: System.Collections.Generic.List`1[[System.Object, mscorlib]]
MethodTable: 04e0856c
EEClass: 04c3e1f0
Size: 24(0x18) bytes
File: c:\Program Files\Microsoft Silverlight\3.0.50106.0\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
04bb5688 4000644 4 System.Object[] 0 instance 07a05bbc _items
04c204d8 4000645 c System.Int32 1 instance 22 _size
04c204d8 4000646 10 System.Int32 1 instance 174 _version
04bb4508 4000647 8 System.Object 0 instance 00000000 _syncRoot
04bb5688 4000648 0 System.Object[] 0 shared static _emptyArray
>> Domain:Value dynamic statics NYI 063ac6f0:NotInit dynamic statics NYI 063aff48:NotInit <<> !da 07a05bbc
Name: System.Object[]
MethodTable: 04bb5688
EEClass: 04bb5618
Size: 144(0x90) bytes
Array: Rank 1, Number of elements 32, Type CLASS
Element Methodtable: 04bb4508
[0] 079fdaec
[1] 079fdaec
[2] 079fdaec
[3] 079fdaec
[4] 079fdaec
[5] 079fdaec
[6] 079fdaec
[7] 079fdaec
[8] 079fdaec
[9] 079fdaec
[10] 079fdaec
[11] 079fdaec
[12] 079def84
[13] 079def84
[14] 079fd528
[15] 079fcb34
[16] 079dd9ec
[17] 079fd528
[18] 079fd528
[19] 079fcb34
[20] 079dd9ec
[21] 079fd528
[22] null
[23] null
[24] null
[25] null
[26] null
[27] null
[28] null
[29] null
[30] null
[31] null

The "!name2ee System.Windows.dll MS.Internal.JoltHelper"
and the "!dumpclass 04e70c90" are useful to dump the static fields of the
MS.Internal.JoltHelper class.
Then you can see that the field named "ControlsWithPendingEvents" is of type
System.Collections.Generic.List`1[[System.Object, mscorlib]].

Then i dumped that object and also dumped the array "_items" inside it with the !da command.

One of the values of the array is a pointer to the Controllo Object (in the output of the !da command is different from the output of the dumpheap -mt command seen before only because the latter is taken from another debugging session).

So we have found where is the handle to the Controllo object.
I think this thing is 'by design' and does not constitute a programming error.
Furthermore it doesn't lead to memory leaks.

My test application has got a simple button on the LayoutRoot that if pressed executes the method:


private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
GC.Collect();
}

If this Button is pressed, after the one that remove the Controllo object from the LayoutRoot, it
removes the handle from the ControlsWithPendingEvents List proving
that is not your code the responsible to holding the handle.

1 comment:

Luke Puplett said...

Just dropping a line to say that I've found a similar thing. I've a toolkit DataGrid which I've subclassed and in my version I observe the DataGrid's own LayoutUpdated event.

The handler does some CPU intensive work which is why I noticed the problem - when the view and part of the app containing the grid is closed, the grid still exists in RAM and the event is still firing, causing lots of CPU cycles. The callstack points back to JoltHelper!