none
WeakReferences in a debug build RRS feed

  • Question

  • I am curious about something that is happening with debug builds.  My CLR version is 2.0.50727.1433.

     

    Consider the following program which constructs numerous instances of a class that derives from DataSet.  The behavior that I am observing occurs with strongly-typed DataSets; here,  I am subclassing DataSet because it is an easy way to demonstrate the issue without using the visual designer to create a "real" strongly-typed DataSet class.

     

    Code Snippet

    Module Module1

     

    Private Class MyDS

    Inherits DataSet

     

    End Class

     

    Sub Main()

    Test()

    GC.Collect()

    Console.WriteLine("Done")

    Console.ReadLine()

    End Sub

     

    Sub Test()

    For i As Integer = 1 To 12345

    Dim d As New MyDS

    d.Dispose()

    Next i

    End Sub

     

    End Module

     

     

    When I run !dumpheap -stat on the process, I see the following line indicating 12345 instances of WeakReference.  This is all after the GC.Collect() has run.  (There are no instances of MyDS reported.)

     

    79104c38    12345       197520 System.WeakReference

     

    When I saw this, I was concerned that the CLR was somehow leaking references.  However, I ultimately discovered that this only seems to happen when I compile the program using the "Debug" build in Visual Studio.  The "Release" build seems to be fine.  Furthermore, if I do a "Debug" build but just construct instances of plain DataSet (not a derived class), it is fine as well.

     

    I never thought that there were any significant differences between "Debug" and "Release" other than a performance hit.  But, based on this, it looks like "Debug" builds may also leak references.  This whole issue has got me really curious to know what exactly is going on "under the covers" here.  Any ideas?

     

     

    Monday, April 14, 2008 1:09 PM

Answers

  • The DataSet constructor uses an internal tracing class, named System.Data.Bid.  The details of which are not visible to prying eyes.  Seeing these weak references only in Debug mode is compelling.  Seeing them survive a GC isn't surprising either, that is after all the very reason for using a WeakReference.  It is only a leak if you get OOM when you loop often enough.
    Tuesday, April 15, 2008 3:19 AM
    Moderator

All replies

  • The DataSet constructor uses an internal tracing class, named System.Data.Bid.  The details of which are not visible to prying eyes.  Seeing these weak references only in Debug mode is compelling.  Seeing them survive a GC isn't surprising either, that is after all the very reason for using a WeakReference.  It is only a leak if you get OOM when you loop often enough.
    Tuesday, April 15, 2008 3:19 AM
    Moderator
  • Thanks, that is very interesting.  By the way, is this an issue that you and others know about, or did you just research it for the first time right now?

     

    > It is only a leak if you get OOM when you loop often enough.

     

    I don't think that the number of WeakReferences decreases; it looks like a leak.  I increased the number of iterations to 10000000 and got that many WeakReferences (160 megabytes worth of WeakReference objects!).  It is so strange that this happens only with derived-DataSet types, not plain DataSet types.

     

    Tuesday, April 15, 2008 12:52 PM
  • Nope, I'm just winging it, trying to explain what you see.  Given that millions of programmers have used DataSet for the past 6 years without a persistent complaint about leaks, one has to reach for alternative explanations.  Synthetic tests like these often give false alarms, you see a similar problem when you create System.Window.Form without ever calling Show() on them (leaking weak references to System.Globalization.Culture).

    So add two zeros to the loop test, go to sleep and tell us what you see when you wake up.  Seeing this only with a derived class of course makes no sense at all.
    Wednesday, April 16, 2008 12:05 AM
    Moderator
  • I changed the loop to 1000000 and put an infinite Do/Loop around the Test() and GC.Collect().

     

    Memory usage climbed to 1461608K and then an OutOfMemoryException occurred.

     

    It seems that the thing to take out of this is that debug builds will leak memory.  Some people might think: "I don't care about the performance difference.  I'll install a debug build in production anyways so it is easier to troubleshoot if I have a problem."  Unfortunately, this will leak memory which will either be wasteful or--worse--eventually crash the program.

     

    So... looks like something to report on Microsoft Connect when I get the chance Smile

     

    Wednesday, April 16, 2008 1:05 AM
  • I got a call stack:

    >    mscorlib.dll!System.WeakReference.WeakReference(object target = {ConsoleApplication1.Module1.MyDS}, bool trackResurrection = false)   
         mscorlib.dll!System.WeakReference.WeakReference(object target) + 0x7 bytes   
         ConsoleApplication10.exe!ConsoleApplication1.Module1.MyDS.MyDS() + 0x64 bytes   
         ConsoleApplication10.exe!ConsoleApplication1.Module1.Test() Line 33 + 0x13 bytes    Basic
         ConsoleApplication10.exe!ConsoleApplication1.Module1.Main() Line 17 + 0x5 bytes    Basic

    Definitely the MyDS constructor doing this.

    Looking at the IL generated by the VB compiler:

    .method public specialname rtspecialname
            instance void  .ctor() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
      // Code size       52 (0x34)
      .maxstack  2
      .locals init (class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.WeakReference> V_0)
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [System.Data]System.Data.DataSet::.ctor()
      IL_0006:  nop
      IL_0007:  nop
      IL_0008:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.WeakReference> ConsoleApplication1.Module1/MyDS::__ENCList
      IL_000d:  stloc.0
      IL_000e:  ldloc.0
      IL_000f:  call       void [mscorlib]System.Threading.Monitor::Enter(object)
      IL_0014:  nop
      IL_0015:  nop
      .try
      {
        IL_0016:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.WeakReference> ConsoleApplication1.Module1/MyDS::__ENCList
        IL_001b:  ldarg.0
        IL_001c:  newobj     instance void [mscorlib]System.WeakReference::.ctor(object)
        IL_0021:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.WeakReference>::Add(!0)
        IL_0026:  nop
        IL_0027:  nop
        IL_0028:  leave.s    IL_0032
      }  // end .try
      finally
      {
        IL_002a:  ldloc.0
        IL_002b:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
        IL_0030:  nop
        IL_0031:  endfinally
      }  // end handler
      IL_0032:  nop
      IL_0033:  ret
    } // end of method MyDS::.ctor

    Yup, there it is, __ENCList.

    The release verion:

    .method public specialname rtspecialname
            instance void  .ctor() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
      // Code size       7 (0x7)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [System.Data]System.Data.DataSet::.ctor()
      IL_0006:  ret
    } // end of method MyDS::.ctor

    Good hits on Googling "__ENCList", it is a helper class to support Edit and Continue.  Microsoft is already aware of the problems it can cause.  Not fixed in VS2008.
    Wednesday, April 16, 2008 2:50 AM
    Moderator