Tuesday, December 6, 2011

Debugging event leaks

Events can be lame. Wonderful, but lame. One thing that bugs me about the subscribe model is that in most of our code we really are just thinking in terms of one subscriber. But the event as a tool encourages thinking only of many subscribers. Into this hole we fall...

The bug is that you have one subscriber to an event, who goes away and comes back later, possibly as a different instance of the same object. Of course he's forgotten to unsubscribe from the event. Meanwhile, the event source is some kind of singleton in your system (in our case it's an expensive-to-create object, lazily initialized and then kept around).

Later, after crash reports come in, you find dozens of zombie consumer objects held in the crypt of the event handler. This wouldn't be so bad, but the zombies clutch in their hands very valuable resources that consume a lot of memory elsewhere.




(output from !gcroot)

...

01acbce4(HistoScanning.Visualization.VizController.VizController)->
14e13e0c(System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]])->
3b110b38(System.Object[])



The object array at 0x3b110b38 is the "crypt," let's see who is in there:


0:000> !dumparray 3b110b38
Name: System.Object[]
MethodTable: 793042f4
EEClass: 790eda64
Size: 144(0x90) bytes
Array: Rank 1, Number of elements 32, Type CLASS
Element Methodtable: 79330740
[0] 01acdb9c
[1] 0e153f30
[2] 02024884
[3] 0203ddc0
[4] 02056444
[5] 31b35dac
[6] 0e51b69c
[7] 323258cc
[8] 3233c5bc
[9] 32644078
[10] 32657a84
[11] 32794b64
[12] 31f88780
[13] 3aa3491c
[14] 3aa7c034
[15] 3adda58c
[16] 3b110b18
[17] 3b127c80
[18] 3b42a008
[19] 149f03fc
[20] 14b0f778
[21] 14e13dec
[22] null
[23] null
[24] null
[25] null
[26] null
[27] null
[28] null
[29] null
[30] null
[31] null


Taking one at random:


0:000> !do 0203ddc0
Name: System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]]
MethodTable: 08cadb2c
EEClass: 790c3518
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79330740  40000ff        4        System.Object  0 instance 0203dcdc _target
7932ff98  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
793333ec  4000101        c        System.IntPtr  1 instance  33801b8 _methodPtr
793333ec  4000102       10        System.IntPtr  1 instance        0 _methodPtrAux
79330740  400010c       14        System.Object  0 instance 00000000 _invocationList
793333ec  400010d       18        System.IntPtr  1 instance        0 _invocationCount
0:000> !do 0203dcdc
Name: HistoScanning.Prost8.ViewModels.PushToolsViewModel
MethodTable: 251f55b0
EEClass: 2306cf38
Size: 68(0x44) bytes
 (C:\Program Files\AMD\HistoScanning\Prost8.ViewModels.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
7932ca34  400007e        8          System.Guid  1 instance 0203dce4 uniqueId
793045f0  400007f        4       System.Boolean  1 instance        1 disposed__BackingField
...
Ah. A disposed PushToolsViewModel, the very definition of zombie, along with 20 friends. Back to the original event handler:
0:000> !do 14e13e0c
Name: System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]]
MethodTable: 08cadb2c
EEClass: 790c3518
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79330740  40000ff        4        System.Object  0 instance 14e13e0c _target
7932ff98  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
793333ec  4000101        c        System.IntPtr  1 instance   95b1e8 _methodPtr
793333ec  4000102       10        System.IntPtr  1 instance 790fe7b0 _methodPtrAux
79330740  400010c       14        System.Object  0 instance 3b110b38 _invocationList
793333ec  400010d       18        System.IntPtr  1 instance       16 _invocationCount
Examine the _invocationList fields of your events to find these characters. Either make sure consumers unsubscribe, or figure out a time when you can blank out the consumers en masse with a statement like
    ...
    ToolChanged = delegate { };
    ...
It's good to do that anyway on dispose of the source object, but in my case we can't dispose of it because of cost.

A few nice links on the topic are here, which is a nice rundown of strategies to deal with the drawbacks of events, and a class from Jeff Richter here.

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home