Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Running out of memory looping through mail items

Hi I have a Outlook com addin that is doing some simple searching tricks for me. I am part way through putting it together but I am having issues with it running out of memory. The process is very simple and basically loops through an outlook folder checking each mailItem for a match. given the loop reinitialize the variables each time I would have expected the garbage collector to keep up but when I watch the memory it loses ~10m/sec until the system is out of memory and I get unhandled exceptions.

This is part of the code

private void FindInFolder(Outlook.MAPIFolder FolderToSearch)
    {
        Outlook.MailItem mailItem;
        Outlook.MAPIFolder ParentFolder;

        int counter = 0;

        StatusBar.Text = "Searching in Folder " + FolderToSearch.FolderPath + "/" + FolderToSearch.Name;
        StatusBar.Update();
        this.Update();

        foreach (COMObject item in FolderToSearch.Items)
        {
            counter++;
            if (counter % 100 == 0)
            {
                StatusBar.Text = FolderToSearch.FolderPath + "/" + FolderToSearch.Name + " item " + counter + " of " + FolderToSearch.Items.Count;
                StatusBar.Update();
                if (counter % 1000 == 0)
                {
                    GC.Collect();
                }
            }
            if (item is Outlook.MailItem)
            {
                mailItem = item as Outlook.MailItem;
                if (IsMatch(mailItem))
                {
                    if (mailItem.Parent is Outlook.MAPIFolder)
                    {
                            ParentFolder = mailItem.Parent as Outlook.MAPIFolder;
                            ResultGrd.Rows.Add(mailItem.EntryID, ParentFolder.FolderPath, mailItem.SenderName, mailItem.Subject, mailItem.SentOn);
                    }
                }
            }
            mailItem = null;
        }
    }

Which calls

        private Boolean IsMatch(Outlook.MailItem inItem)
    {
        Boolean subBool = false;
        Boolean NameBool = false;

        try
        {
            if (null != inItem)
            {
                if (SubjectTxt.Text != "")
                {
                    if (inItem.Subject.Contains(SubjectTxt.Text))
                    {
                        subBool = true;
                    }
                }
                else
                {
                    subBool = true;                    
                }

                if (NameTxt.Text != "")
                {
                    if (inItem.Sender != null)
                    {
                        if (inItem.Sender.Name.Contains(NameTxt.Text))
                        {
                            NameBool = true;
                        }
                    }
                }
                else 
                {
                    NameBool = true;
                }

                return subBool && NameBool;

            }
        }
        catch (System.Runtime.InteropServices.COMException ce)
        {
            if (ce.ErrorCode == -2147467259)
            {
                //DO nothing just move to the next one
            }
            else
            {
                MessageBox.Show("Crash in IsMatch error code = " + ce.ErrorCode + " " + ce.InnerException);
            }
        }
        return false;
    }

Please excuse all the error catching part at the bottom and the GC.collect they are some of my attempts to work out what is wrong and free up memory.

Note also FindInFolder is called by a new thread so I can interact with results while it continues to search.

What I have tried so far:

Making variables local to function not class so the are retrievable by G, however the most used variable in 'item' as it is part of foreach it must be declared that way.

every 1000 mailItems do a manual GC, this made no difference at all.

For some reason it needs alot of memory just looping through the items and GC never frees them up.

Please also note I am using netoffice not VSTO for Com addin.

like image 731
AndrewT Avatar asked Mar 16 '23 17:03

AndrewT


2 Answers

First of it all: This is NetOffice code and you dont need Marshal.ReleaseComObject in NetOffice. (Moreover, its useless to call ReleaseComObject here) Use Dispose() for the instance instead.

Keep in your mind: NetOffice handle COM proxies for you (Thats why its allowed to use two 2 dots in NetOffice). In your case its internaly stored as: // FolderToSearch -- Items --Enumerator -- Item -- Item -- ....

Use item.Dispose() at the end of each loop to remove/free the item instance or use the following after foreach

FolderToSearch.Dipose() // dispose folder instance and all proxies there comes from

FolderToSearch.DisposeChildInstances() // dispose all proxies there comes from but keep the folder instance alive

Next: The Items enumerator here is a custom enumerator(given by NetOffice) However it works fine with small amount of items but dont do this in a more heavier scenario(may exchange server and thousands of items). The local workstation/program can't handle this in memory. For this reason Microsoft provides only the well kown GetFirst/GetNext pattern. In NetOffice it looks like:

Outlook._Items items = FolderToSearch.Items;
COMObject item = null;
do
{
    if (null == item)
       item = items.GetFirst() as COMObject;
    if (null == item)
       break;

    // do what you want here

    item.Dispose();
    item = items.GetNext() as COMObject;
} while (null != item);

This should works as well.

like image 80
IDisposable Avatar answered Mar 27 '23 12:03

IDisposable


When working with COM objects from C#, there were 2 tricks that I used to prevent memory and COM object reference counts from building:

  1. Use System.Runtime.InteropServices.Marshal.ReleaseComObject() to release COM objects as you are done with them. This forces a COM "release" on the object.
  2. Do not foreach to iterate through a COM collection. foreach holds on to an enumerator object, which prevents other objects from being released.

So, instead of this:

foreach (COMObject item in FolderToSearch.Items)
{
    // ....
}

do this:

Items items = FolderToSearch.Items;
try
{
    for (int i = 0; i < items.Count; ++i)
    {
        COMObject item = items[i];
        try
        {
            // work
        }
        finally
        {
            System.Runtime.InteropServices.Marshal.ReleaseComObject(item);
        }
    }
}
finally
{
    System.Runtime.InteropServices.Marshal.ReleaseComObject(items);
}

These tips helped me reduce memory and object consumption.

Disclaimer: I cannot attest to whether this is good practice or not though.

like image 28
Matt Houser Avatar answered Mar 27 '23 12:03

Matt Houser