love to code .net

blogging mostly about one of my favorite subjects, programming

CSLA.NET Memory Leak

Background

There is a related post here on the CSLA.NET Forums.  The forum doesn’t allow images.  The Ants Memory Profiler screen captures may be useful so they are in this post.  A bunch of images would just look weird, so this post contains more detail.

While writing a replacement application for an Access DB I wrote about 10 years ago for a client, I discovered a memory leak when importing data from the access source.  The application is not just a replacement but an upgrade to both the DB and the Business Logic.  It’s now a ASP.NET MVC based using CSLA.NET with a SQL DB.  The DB import isn’t just copying the tables over directly; the entire table structure has been changed.  To limit the amount of dedicated code, the import uses the CSLA Business Objects.

During development a situation occurred where remote users of the Access application weren’t able to consistently use it, and eventually not at all.  So that meant a slight refocus on the development effort to get the portion that they use up an running.  Unfortunately when I tried to import the full database, the application crashed several hours into the import due to a OutOfMemoryException.  This was two weeks into what I estimated to be a three week effort.  The next five days were spent finding where the memory was leaking.  It probably would have gone faster if I had just gotten Ants Profiler from the start, but I was convinced it had to be something I was doing in my Business Objects and thought I could use the memory profiler built into Visual Studio 2008.

On a side note in case your wondering why I didn’t just go to a 64-bit where more memory is available and worry about the leak later?  I would have but their isn’t a 64-bit Access driver available.

Tracking the issue down

Due to the OutOfMemoryException being thrown, a few modifications were made to the import routine to limit what was being imported.  Only about 8 MB of data is being imported.  The allocated memory before beginning the import is just under 1 MB (see the next image).

1 Startup Memory Summary

After running a portion of the import and removing any references in my code the allocated memory was 20MB (see the next image).

 2 Leak Summary

Here you see the number of live instances after the importer completed.

9 Leak Live Objects

Look around at the Business Objects that were still around in memory, showed that roughly 12MB of memory was allocated in references through instances of the CategoryLocationHistory class (see the next image). 

  4 CatLocHistoryInstance

These next three images show the different classes that go up the reference chain to find the source of the events.

 5 PropertyChangedEventHandler

 6 Object Array

 7 PropertyChangedEventHandler

The source of the event was a Read Only Object.

 8 CategoryLocationRO

Workaround

Noticing that the only Business Objects still in memory had a Read Only object in them I decided to test a hunch and changed my code to not use the managed backing field for Read Only objects and to not use the LoadProperty and SetProperty methods.  This worked, as you can see in this next image, only around 7MB of memory is allocated.

3 Plugged Summary

The full import ran shortly thereafter and completed successfully.  The rest of the effort was completed, finishing only about 3 days after what I had estimated.

VSLive! Las Vegas 2009

VSLive! was only a few days away.  Rocky was scheduled for a few of the sessions and I decided to hold off on posting to the forum until I returned.  I had a few minutes to talk with Rocky while in Vegas.  I mentioned my use case to Rocky and since he didn’t look at me like why would you ever want to do that, I figured it wasn’t something he thought was unreasonable to do. 

The particular use case is that there are large number of objects that don’t change very often.  The object references inside the Business Object can be swapped or removed, the object itself can not changed.  This means we have BusinessBase derived objects that have ReadOnly objects in them.

He asked me to do a little more digging around as he wasn’t aware of any leaks.  I told him that from what I remember in the Reference List provided by Ants that it was looking like the events were keeping the objects around.  It was a short discussion as Rocky usually has a small crowd following him who all want to talk with him.

Testing the Theory

After getting back from VSLive!, I took another look at the memory profiler results.  Looking at the code and I made some changes to a copy of CSLA.NET to see if it was in fact the events.  After a few hours (running the modified import takes about half an hour) and looking at the profiler results I concluded that three events were attaching to the ReadOnlyObject from BusinessBase and keeping the object tree alive.  The three events were BusyChanged, UnhandledAsyncException, and PropertyChanged.  Here is the code I used to prevent BusinessBase from registering for those events.  Note equivalent code is needed in the OnRemoveEventHooks method to prevent unregistering from those events, as they are no longer being registered.

1: protected virtual void OnAddEventHooks(IBusinessObject child)
2:
3:   bool isReadOnly = child is IReadOnlyObject; 
4:   INotifyBusy busy = child as INotifyBusy;
5:   if (busy != null && !isReadOnly)
6:     busy.BusyChanged += new BusyChangedEventHandler(Child_BusyChanged); 
7:
8:   INotifyUnhandledAsyncException unhandled = child as INotifyUnhandledAsyncException;
9:   if (unhandled != null && !isReadOnly)
10:     unhandled.UnhandledAsyncException += new EventHandler<ErrorEventArgs>(Child_UnhandledAsyncException); 
11:
12:   INotifyPropertyChanged pc = child as INotifyPropertyChanged;
13:   if (pc != null && !isReadOnly)
14:     pc.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged); 
15:
16:   IBindingList bl = child as IBindingList;
17:   if (bl != null)
18:     bl.ListChanged += new ListChangedEventHandler(Child_ListChanged); 
19:
20:   INotifyChildChanged cc = child as INotifyChildChanged;
21:   if (cc != null
22:     cc.ChildChanged += new EventHandler<ChildChangedEventArgs>(Child_Changed);
23: }

After implementing this code and switching back to using the managed backing fields, the importer was getting similar memory results to the workaround previously mentioned.  Here you see the number of live instances for my Business Objects is now zero.

10 Plugged Live Objects

Conclusion

It will be interesting to see what Rocky thinks of this.  His extensive knowledge of his framework may lead him in a different direction then I took.  He also would know the full implications of not subscribing to those events.

I still have to run a full import using the code above.  It takes 8-9 hours to run and I just haven’t had time yet.  I should have time this weekend.

Let me know what you think.

kick it on DotNetKicks.com

Comments

Add comment


(Will show your Gravatar icon)  

  Country flag

biuquote
  • Comment
  • Preview
Loading