-1

I apologize in advance for the long post...

I'm looking for some advice on how to deal with a problem that I am encountering with some software that I wrote for a client. In short, the client has a third-party document management system that stores their shipping orders, invoices, etc. When new documents are created, the client needs copies saved and printed in order to be shipped with the merchandise that they sell. The third-party software manufacturer makes an SDK with a .NET DLL that allows C# and VB.NET programs to query and save documents out. So I wrote them a program that uses this DLL to periodically scan the system, and when it finds new documents, my program will save them to a temp directory and print them. Everything works well, except that the SDK wasn't very well made, so whenever the method to save a document is called, a bunch of stuff gets loaded into RAM that the third-party SDK doesn't get rid of (i.e. it doesn't manage memory very well). Sometimes the client will run large batches and this accumulation of RAM will slow down their system, and has caused Out of Memory exceptions a couple times. I wrote some sample code to simulate the problem. It does take a little imagination, but it'll give you a good idea of the problem that I have to overcome. The first code sample simulates a class in the third-party's DLL.

The "DLL" Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace DisposeSample
{
    public class OtherGuysDllClass
    {
        /*
            This is intended to simulate the SDK class library where the memory 
            leak occurs.  It ships with the third-party software that it integrates with, and 
            I can't change it.  Pretend this is a .dll that I referenced in my project.
        */

        public OtherGuysDllClass()
        {
            /*
                I wrote this to simulate a process that would build up in memory over time.  The SDK doesn't 
                do this per se, but something similar that causes junk to accumulate in RAM over time.
            */

            StreamWriter sw = new StreamWriter(Environment.CurrentDirectory + "\\output.txt");
            sw.WriteLine(DateTime.Now.ToString());
            sw.Close();
        }

    }
}

You can see that the code in the above class contains a StreamWriter object that was not properly disposed of, so it will cause some junk to be left in memory. Again, the DLL doesn't do this exactly, but something that will cause a memory problem like the sample above.

I also wrote a WinForms app with a timer control to periodically create a new object from the above class that simulates the program that I wrote for the client.

My program:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace DisposeSample
{
    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            timer1.Interval = 500;
            timer1.Enabled = true;
        }


        /*
            Timer1 Tick Event Handler
            (This Timer Control was dragged and dropped onto the form in the designer).
        */
        private void timer1_Tick(object sender, EventArgs e)
        {
            OtherGuysDllClass dllObj = new OtherGuysDllClass();
            dllObj = null;

            /*
                Is there any way to make the program wipe the ddlObj and everything it created from 
                memory?  I tried calling GC.Collect(), but it didn't help much.
            */
        }

    }
}

So imagine that you were given a DLL that contains a class like the top one, and you had a program running round-the-clock that periodically creates instances of the class. How would you get around the problem of the gradual accumulation of objects in memory. Any suggestions/advise would be appreciated.

Thanks!

Kevin Herrick
  • 413
  • 1
  • 6
  • 16
  • 2
    *Assuming* there are no maintained strong references (that is, we can "trust" OtherGuysDllClass) then there will be no problem - and .NET will correctly recycle (to itself) memory. (The fact that "GC.Collect didn't help much" indicates some objects are *not* reclaimable; properly implemented Streams would have *eventually* finalized out the native resources.) However, there is buggy code so *iff* this is the case .. create a new App Domain and have the application "soft restart" itself? (You can also call out to another restartable App Domain, to not effect the primary application.) – user2864740 Feb 23 '15 at 21:17
  • By chance does the "other guy's DLL" implement `IDisposable`? Perhaps it is not buggy code, but your code is not cleaning up what it should...? – NightOwl888 Feb 23 '15 at 21:21
  • Lol! Well, I don't know if we can "trust" the other guy's class, but I have no choice. It's not like they're going to start using a different ECM software anytime soon, so I guess I have to. – Kevin Herrick Feb 23 '15 at 21:21
  • 2
    @KevinHerrick I've used commercial (and "supported") libraries where I've had to run them in a separate App Domain and help them "wash their laundry". It's annoying, but doable. (Also, I would say it is more common than not to have very-long-running processes have some way of recycling the tasks - to allocate an entirely new "process" with a fresh slate and pool of memory.) – user2864740 Feb 23 '15 at 21:22
  • Consider downloading ANTS Memory profiler to confirm what objects are hanging around and where they're rooted. The first step to resolve a memory issue is determining what's sitting around. – Adam47 Feb 23 '15 at 21:24
  • I could try that. It's not IDisposable - I checked for that. If it helps, all their class objects are COM objects, and the SDK documentation recommends that you call Marshal.ReleaseComObject() to release them. In this particular case, it clears a big chuck, but not everything that's in memory. – Kevin Herrick Feb 23 '15 at 21:26
  • @KevinHerrick You may want to include the name of the ECM you are working with. Others that have worked with it might be able to help you. – juharr Feb 23 '15 at 21:28
  • `sw.Close();` is good enough. The sample code does not demonstrate any problem. – H H Feb 23 '15 at 21:35
  • @user2864740 With the AppDomain route, did you create an executable to do the job, then execute and unload it? – Kevin Herrick Feb 23 '15 at 22:01
  • @KevinHerrick Could I suppose, but I'd recommend (and have done) something like [this answer](http://stackoverflow.com/a/2009473/2864740) - that is a normal, or even the same, assembly will do fine (just make to not load the "trusted assembly" too soon!). The design will determine if the entire task (probably easier, esp. with "clean restarts") is kicked off across the boundary or just the special assembly ;-) – user2864740 Feb 23 '15 at 23:46

1 Answers1

0

I think the new AppDomain is money! ++ user2864740

Like you said, this approach is a bit RubeGoldberg-ish, but it gets the job done. It executes fast enough, and the memory usage stays steady.

Here's my revised solution. The project contains a reference to the .dll that I'm substituting for the third-party .dll that contains the memory leak. Here's the code for that:

Code for Simulated Third-Party DLL -

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace OtherGuysDll
{
    public class OtherGuysCode
    {

        /// <summary>
        /// Example of an IDisposable that doesn't run in a using statement or call the Dispose method in a finally 
        /// block.  Used to simulate a third-party .dll that doesn't properly deallocate resources and can cause 
        /// problematic accumulation in RAM over time.
        /// </summary>
        public OtherGuysCode()
        {
            /*
                The code below demonstrates a chunk of unmanged code that doesn't deallocate resources 
                properly.  Feel free to substitute any code that will cause a memory leak below if you'd prefer a 
                different example.
            */

            StreamWriter sw = new StreamWriter(Environment.CurrentDirectory + "\\output.txt");
            sw.WriteLine(DateTime.Now.ToString());
            sw.Close();
        }

    }
}

Again, this is used as a substitution for a .dll in a third-party's SDK. I have no control over this code and can't change it. I added to my project a separate class that implements the MarshalByRefObject class and contains a public method to create a single instance of the problem .dll:

Code for the Create New Domain Instance Class -

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DisposeSample
{
    /*
        A new instance of this class is created by the New Domain object 
        in each of the Form1's Scan Timer Tick Event.
    */
    public class CreateNewDomainInstanceClass : MarshalByRefObject
    {

        /// <summary>
        /// This method creates a new instance of the problem DLL.
        /// </summary>
        public void RunOtherGuysCode()
        {
            // Create a new instance of the Other Guy's Code class
            OtherGuysDll.OtherGuysCode ogc = new OtherGuysDll.OtherGuysCode();
        }

    }
}

Finally, I revised the ScanTimer Tick Event to handle the Create New Domain Instance Class in a new domain with every tick:

Code for the Form that Contains the ScanTimer -

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace DisposeSample
{
    public partial class Form1 : Form
    {

        // Default Constructor
        public Form1()
        {
            InitializeComponent();
        }


        // Form Load Event Handler
        private void Form1_Load(object sender, EventArgs e)
        {
            timer1.Interval = 500;
            timer1.Enabled = true;
        }



        /*
            Timer1 Tick Event Handler
            (This Timer Control was dragged and dropped onto the form in the designer).
        */
        private void timer1_Tick(object sender, EventArgs e)
        {
            // Create a new App Domain object
            AppDomain newDomain = AppDomain.CreateDomain("Other Guy's DLL");

            // Get the Assembly Name of the Create New Domain Instance Class
            string aName = typeof(CreateNewDomainInstanceClass).Assembly.FullName;

            // Get the Full Name of the Create New Domain Instance Class
            string tName = typeof(CreateNewDomainInstanceClass).FullName;

            // Create a new instance of the Create New Domain Instance Class in the new App Domain
            CreateNewDomainInstanceClass ndInstance = (CreateNewDomainInstanceClass)newDomain.CreateInstanceAndUnwrap(aName, tName);

            // Run the public method in the new instance
            ndInstance.RunOtherGuysCode();

            // Unload the new App Domain object
            AppDomain.Unload(newDomain);

            // Call an immediate Garbage Collection to ensure that all unused resources are removed from RAM
            GC.Collect();
        }

    }
}

This approach seems to get the job done pretty well, and it's as straight-forward as I can make it. I hope this helps anyone else who may have a similar problem. And again, special thanks to user2864740 for suggesting the AppDomain angle.

Kevin Herrick
  • 413
  • 1
  • 6
  • 16