Memory Leak when using DirectorySearcher.FindAll()

12,582

Solution 1

As strange as it may be, it seems that the memory leak only occurs if you don't do anything with the search results. Modifying the code in the question as follows does not leak any memory:

using (var src = mySearcher.FindAll())
{
   var enumerator = src.GetEnumerator();
   enumerator.MoveNext();
}

This seems to be caused by the internal searchObject field having lazy initialization , looking at SearchResultCollection with Reflector :

internal UnsafeNativeMethods.IDirectorySearch SearchObject
{
    get
    {
        if (this.searchObject == null)
        {
            this.searchObject = (UnsafeNativeMethods.IDirectorySearch) this.rootEntry.AdsObject;
        }
        return this.searchObject;
    }
}

The dispose will not close the unmanaged handle unless searchObject is initialized.

protected virtual void Dispose(bool disposing)
{
    if (!this.disposed)
    {
        if (((this.handle != IntPtr.Zero) && (this.searchObject != null)) && disposing)
        {
            this.searchObject.CloseSearchHandle(this.handle);
            this.handle = IntPtr.Zero;
        }
    ..
   }
}

Calling MoveNext on the ResultsEnumerator calls the SearchObject on the collection thus making sure it is disposed properly as well.

public bool MoveNext()
{
  ..
  int firstRow = this.results.SearchObject.GetFirstRow(this.results.Handle);
  ..
}

The leak in my application was due to some other unmanaged buffer not being released properly and the test I made was misleading. The issue is resolved now.

Solution 2

The managed wrapper doesn't really leak anything. If you don't call Dispose unused resources will still be reclaimed during garbage collection.

However, the managed code is a wrapper on top of the COM-based ADSI API and when you create a DirectoryEntry the underlying code will call the ADsOpenObject function. The returned COM object is released when the DirectoryEntry is disposed or during finalization.

There is a documented memory leak when you use the ADsOpenObject API together with a set of credentials and a WinNT provider:

  • This memory leak occurs on all versions of Windows XP, of Windows Server 2003, of Windows Vista, of Windows Server 2008, of Windows 7, and of Windows Server 2008 R2.
  • This memory leak occurs only when you use the WinNT provider together with credentials. The LDAP provider does not leak memory in this manner.

However, the leak is only 8 bytes and and as far as I can see you are using the LDAP provider and not the WinNT provider.

Calling DirectorySearcher.FindAll will perform a search that requires considerable cleanup. This cleanup is done in DirectorySearcher.Dispose. In your code this cleanup is performed in each iteration of the loop and not during garbage collection.

Unless there really is an undocumented memory leak in the LDAP ADSI API the only explanation I can come up with is fragmentation of the unmanaged heap. The ADSI API is implemented by an in-process COM server and each search will probably allocate some memory on the unmanaged heap of your process. If this memory becomes fragmented the heap may have to grow when space is allocated for new searches.

If my hypothesis is true, one option would be to run the searches in a separate AppDomain that then can be reclaimed to unload ADSI and recycle the memory. However, even though memory fragmentation may increase the demand for unmanaged memory I would expect that there would be an upper limit to how much unmanaged memory is required. Unless of course you have a leak.

Also, you could try to play around with the DirectorySearcher.CacheResults property. Does setting it to false remove the leak?

Solution 3

Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.

http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.findall.aspx

EDIT:

I've been able to repro the apparent leak using perfmon, and adding a counter for Private Bytes on the process name of the test app (Experiments.vshost for me )

the Private Bytes counter will steadily grow while the app is looping, it starts around 40,000,000, and then grows by about a million bytes every few seconds. The good news is the counter drops back to normal (35,237,888) when you terminate the app, so some sort of cleanup is finally occurring then.

I've attached a screen shot of what perfmon looks like when its leakingperfmon screenshot of memory leak

Update:

I've tried a few workarounds, like disabling caching on the DirectoryServer object, and it didn't help.

The FindOne() command doesn't leak memory, but i'm not sure what you would have to do to make that option work for you, probably edit the filter constantly, on my AD controller, there is just a single domain, so findall & findone give the same result.

I also tried queuing 10,000 threadpool workers to make the same DirectorySearcher.FindAll(). It finished alot faster, however it still leaked memory, and actually private bytes went up to about 80MB, instead of just 48MB for the "normal" leak.

So for this issue, if you can make FindOne() work for you, you have a workaround. Good Luck!

Solution 4

Have you tried using and Dispose()? Info from here

Update

Try calling de.Close(); before the end of the using.

I don't actually have an Active Domain Service to test this on, sorry.

Share:
12,582

Related videos on Youtube

Can Gencer
Author by

Can Gencer

Software Engineer who wants to learn more about everything software.

Updated on May 28, 2022

Comments

  • Can Gencer
    Can Gencer almost 2 years

    I have a long running process that needs to do a lot of queries on Active Directory quite often. For this purpose I have been using the System.DirectoryServices namespace, using the DirectorySearcher and DirectoryEntry classes. I have noticed a memory leak in the application.

    It can be reproduced with this code:

    while (true)
    {
        using (var de = new DirectoryEntry("LDAP://hostname", "user", "pass"))
        {
            using (var mySearcher = new DirectorySearcher(de))
            {
                mySearcher.Filter = "(objectClass=domain)";
                using (SearchResultCollection src = mySearcher.FindAll())
                {
                }            
             }
        }
    }
    

    The documentation for these classes say that they will leak memory if Dispose() is not called. I have tried without dispose as well, it just leaks more memory in that case. I have tested this with both framework versions 2.0 and 4.0 Has anyone run into this before? Are there any workarounds?

    Update: I tried running the code in another AppDomain, and it didn't seem to help either.

    • Can Gencer
      Can Gencer about 13 years
      That is just to illustrate the problem, in the real application it is not like that, obviously.
    • Cheng Chen
      Cheng Chen about 13 years
      How can you "noticed a memory leak in the application"?
    • Larry
      Larry about 13 years
      Unfortunately, using while(true)... at this place would prevent the DirectorySearcher and the DirectoryEntry being disposed correctly. You might want to put it at the first level of "using" instead, and check what happens.
    • Can Gencer
      Can Gencer about 13 years
      actually I have tried that first, with same results. It gives the same result either way. Edited my code in the question though and moved the loop outside.
    • Haplo
      Haplo about 13 years
      @Can Gencer - have you tried to inspect the memory usage with WinDebug and managed extensions? I have successfully troubleshooted memory leaks using it.
    • Can Gencer
      Can Gencer about 13 years
      @Haplo, I've used WinDbg and also Ants profiler, and the problem seems to be in the unmanaged memory part, which keeps growing.
    • Andrew Barber
      Andrew Barber about 13 years
      @Can Gencer - you say "in the real application it is not like that, obviously". What may not be so obvious is that we really can't offer any real clues unless we know what your real code is like.
    • Can Gencer
      Can Gencer about 13 years
      @Andrew, in my earlier code the while loop was around the findall(), which is not a very realistic scenario. The point of the code above is to illustrate the memory leak, which seems to happen everytime FindAll() is called. It is not meant to show a piece of the real application.
    • Andrew Barber
      Andrew Barber about 13 years
      @Can Gencer - again... without seeing actual code, all we are doing is making wild guesses in the dark.
    • Can Gencer
      Can Gencer about 13 years
      @Andrew That is the actual code leaking the memory. You can run it, and it will leak.. If I can prevent the code above from leaking, then I can use it in my real code, which has a lot of extra code not relevant for this question, that is why I skipped it.
    • user492238
      user492238 about 13 years
      which of these objects is leaking? Did you find out by using SOS.dll f.e.?
    • Can Gencer
      Can Gencer about 13 years
      Ants profiler shows "unmanaged memory" as the part that is increasing. It is not directly something within the GC's reach..
    • Ian Ringrose
      Ian Ringrose almost 13 years
      I recall I hit this problem in the .net 2 days, you may be better of using the LDAP classes to talk to ActiveDirectory, rather then the old ADSI mess!
    • Can Gencer
      Can Gencer almost 13 years
      @Ian Ringrose, which LDAP classes are you talking about?
  • Can Gencer
    Can Gencer about 13 years
    puttng the dispose in the using block is in effect calling Dispose twice.. and it doesn't seem to have any effect (as expected). I also went through the dispose code for the classes using .NET Reflector which both check if the object is disposed already and do nothing otherwise.
  • Larry
    Larry about 13 years
    It would be interresting to check your source code what is in your least level of Using: the example in the link shows other AD objects which also needs a Dispose(), like Properties object.
  • Can Gencer
    Can Gencer about 13 years
    The Properties object doesn't implement IDispoable, it is just DirectorySearcher, DirectoryEntry and SearchResultCollection.
  • Can Gencer
    Can Gencer about 13 years
    The close doesn't have any effect. I've verified this using Reflector. Both Close() and Dispose() call the Unbind() function and are essentially the same.
  • William Mioch
    William Mioch about 13 years
    Is there a test Active Domain Service we could connect to on the web somewhere to test the code ourselves? Or is it simple to setup locally?
  • Can Gencer
    Can Gencer about 13 years
    Not that I'm aware of. What you can do is download the Server 2008 R2 180 day trial Virtual machine from Microsoft and add "domain controller" role to the server. microsoft.com/windowsserver2008/en/us/trial-software.aspx
  • Can Gencer
    Can Gencer almost 13 years
    the "using" scope is equivalent to calling dispose, and also it doesn't have any effect to call dispose more than once.
  • Can Gencer
    Can Gencer almost 13 years
    nice find! that's exactly it. It is very little each time but builds up over time and there was definately a leak somewhere and I did create a new DirectoryEntry each time (with credentials).
  • Can Gencer
    Can Gencer almost 13 years
    @Martin, actually reading the link again, I don't use the WinNNT provider, it's using the LDAP provider instead.. Also the leak seems to occur when you do a search, not use a new DirectoryEntry.. (if I change the loop as such). Seems I got excited a bit prematurely. But probably there is some other memory leak in the COM API..
  • Ivan Bohannon
    Ivan Bohannon almost 13 years
    I hope your right, the MSDN document seems to indicate an explicit dispose call is needed because the collection has unmanaged resources. I'll test it out this morning.
  • Can Gencer
    Can Gencer almost 13 years
    CacheResults did not seem to have any effect. Each FindAll call seems to increase memory usage by around 500-600 bytes on average (over 1000 runs), (measured using calling Gc.Collect(), GC.WaitForPending() just before measuring the memory), and using Process.GetCurrentProcess().PrivateMemorySize64 to get the memory size.
  • Ivan Bohannon
    Ivan Bohannon almost 13 years
    it seems that manual dispose or using makes no difference on the leak. I'll see if I can figure out exactly what kind of memory is leaking.
  • Ivan Bohannon
    Ivan Bohannon almost 13 years
    I also verified that if you comment out the call to FindAll(), the leak stops. Of course the code is useless at this point :)
  • Ian Ringrose
    Ian Ringrose almost 13 years
    A seperate AppDomain would not help, as memory is collected on a process wide bases, so a seperate process would be needed.
  • Martin Liversage
    Martin Liversage almost 13 years
    @Ian Ringrose: Unloading an AppDomain will also unload any COM DLL's that was loaded into that AppDomain. However, I'm not quite sure what will happen with any unmanaged heap memory allocated by that DLL. Chances are that it relies on a C++ library for memory allocation and this library and its associated resources may still be loaded into the process after the AppDomain is unloaded. Obviously the COM DLL will delete/free the memory when it unloads (unless it has a leak) but the segment may still be mapped into the process.
  • Ian Ringrose
    Ian Ringrose almost 13 years
    @Martin From I post as a C++ programmer, I recall that most delete/free imps do not give any memory back to the OS and don't cope well with fagments. You also have to hope another COM dll that uses the same C/C++ runtime has not been loaded by a different app domain - too many "ifs" for my liking!
  • Can Gencer
    Can Gencer almost 13 years
    @Martin, the mystery is solved. It seems to leak memory only if you don't do anything with the search result. See my answer.
  • Martin Liversage
    Martin Liversage almost 13 years
    From your explanation it looks like a bug in the .NET library. You should consider fileing a Connect bug: connect.microsoft.com/VisualStudio
  • Abhijeet Patel
    Abhijeet Patel about 12 years
    Isn't using the enumerator the same as doing a foreach over the SearchResultCollection?
  • Can Gencer
    Can Gencer about 12 years
    @AbhijeetPatel yes there is no difference. It was just there to illustrate the bug.
  • Termit
    Termit over 9 years
    I was dispoding everything but my program was still leaking some memory reported by a profiler tool. Setting DirectorySearcher.CacheResults to false did the trick. Thanks