Thursday, December 10, 2009

Office Interop Object Collection Technique – Ghost Excel Instance

Recently, we wrote an add-in for Microsoft PowerPoint to automate several routine tasks for our business users. While developing this add-in we required to open a temp workbook in a new excel application instance to perform several tasks. Once these tasks were completed the temp workbook was supposed to be closed and the excel application needed to be quit. However to our amazement, we found that in spite of quitting excel application by calling Quit() on excel application object we found it to be silently running in task manager. It seemed that this process lives in memory forever until the AppDomain on which add-in is loaded is torn.

After this we researched a bit to see as to what is really causing the Excel process to live in memory forever. And to answer this and several other basic questions, I wrote a small windows console application as my POC. Below is how the source appears.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
using System.Reflection;
using System.Runtime.InteropServices;
namespace GhostXLInstance
{
  class Program
  {
        static void Main(string[] args)
        {
            Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application();
            Excel.Workbook newWorkbook = xlApp.Workbooks.Add(Missing.Value);
            int workbookRCWCounter = Marshal.ReleaseComObject(newWorkbook);
            Console.WriteLine("RCW counter for workbook :: " + workbookRCWCounter.ToString());
            xlApp.Quit();
            int excelRCWCounter = Marshal.ReleaseComObject(xlApp);
            Console.WriteLine("RCW counter for XL application :: " + excelRCWCounter.ToString());
            Console.Read();
        }
    }
}
In above code, I am creating a new excel application, adding new workbook in it and then releasing both application and workbook references (observe that we explicitly created only two interop references xlApp and newWorkbook).

After executing the example, you will see two lines in console telling you the RCW count for each object we released. Viola!!!  It is zero which apparently means that all the objects we referenced are released. However, if you observe the task manager now the excel instance is still running. So the question still remains the same – What is holding the excel process?

Well, then I got a fantastic article http://blogs.msdn.com/geoffda/archive/2007/08/31/the-designer-process-that-would-not-terminate.aspx which explained the phenomena. So here is the phenomenon in simple words.
If you closely observe our example, we are actually getting three references. The third reference is implicitly created when we used the application’s Workbooks collection to add new workbook.

Excel.Workbook newWorkbook = xlApp.Workbooks.Add(Missing.Value);
Observe that xlApp.Workbooks actually returns Microsoft.Office.Interop.Excel.Workbooks reference. Wow! So did we got the cause of immutability of excel process? Yes! So this uninvited third reference lives forever in the memory and causes the excel process to wait for its garbage collection.
Now since we got the cause, lets modify our previous code to release third reference even.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
using System.Reflection;
using System.Runtime.InteropServices;
namespace GhostXLInstance
{
    class Program
    {
        static void Main(string[] args)
        {
            Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application();
            Excel.Workbooks workbooks = xlApp.Workbooks;
            Excel.Workbook newWorkbook = workbooks.Add(Missing.Value);           
            int workbookRCWCounter = Marshal.ReleaseComObject(newWorkbook);
            Console.WriteLine("RCW counter for workbook :: " + workbookRCWCounter.ToString());
            int workbooksRCWCounter = Marshal.ReleaseComObject(workbooks);
            Console.WriteLine("RCW counter for workbooks :: " + workbooksRCWCounter.ToString());
            xlApp.Quit();
            int excelRCWCounter = Marshal.ReleaseComObject(xlApp);
            Console.WriteLine("RCW counter for XL application :: " + excelRCWCounter.ToString());
            Console.Read();
        }
    }
}
Finally in modified code, I am collecting the xlApp.Workbooks reference to a local variable and then releasing it. And now executing this code would release the ghost application while console application is waiting at Console.Read();

Conclusion:
Well so from our POC it became apparent that the so called collection references (like workbooks, shapes, charts …) needs to be explicitly referenced in local variable and garbage collected to neatly exit excel process.

Note: In my next posting I will detail the right way to release Interop references.

No comments:

Post a Comment