[GIS] IFeatureCursor.NextFeature not releasing memory

arcobjects

I've got a non recycling cursor retrieving features into a List<IFeature>.

When I populate the list, I can see my memory usage climbing up. When I discard my list (or it goes out of scope), the memory doesn't go down again, even when explicitly calling the garbage collector.

I've tried looping through the list and calling ReleaseComObject on each IFeature, also to no avail.

This is an issue, as my app does this whenever a given user action is performed, and the memory usage just keeps climbing, till there's no memory left on the machine and it crashes.

This is driving me nuts. Any ideas?

Following further investigation, it appears as though while there is a tiny amount of unmanaged memory not being released it's not enough to worry about, and it's not accumulating. It appears to be a one off hit (no change when running the test through a loop). For completeness, I'll include my test code here, for anyone else having issues. It's a slightly modified version of Kirk's, also showing total process memory (not just managed, as the IFeature data sits in unmanaged memory). The end result is that there must be something else hanging onto the features in my code, so will have to look elsewhere.

My understanding of the IRow (and IFeature) is that they're singletons, so even if I nullify the managed reference to a feature, if any other reference exists, it won't release the unmanaged memory. As a result you need to be very careful in ensuring all references are removed. I hope my pain helps someone else.

    public static void RunTest( string fgdbPath, string className)
    {
        var ws = (IFeatureWorkspace)OpenFromPath(fgdbPath);
        var fc = ws.OpenFeatureClass(className);

        long managedMem1 = GC.GetTotalMemory(true);
        long unmanagedMem1 = MemoryUsage();

        List<IFeature> feats = GetAllFeats(fc, false);

        long featCount = feats.Count;
        long managedMem2 = GC.GetTotalMemory(true);
        long unmanagedMem2 = MemoryUsage();

        feats = null;

        long managedMem3 = GC.GetTotalMemory(true);
        long unmanagedMem3 = MemoryUsage();

        GC.Collect();

        long managedMem4 = GC.GetTotalMemory(true);
        long unmanagedMem4 = MemoryUsage();

        System.Runtime.InteropServices.Marshal.ReleaseComObject(ws);

        long managedMem5 = GC.GetTotalMemory(true);
        long unmanagedMem5 = MemoryUsage();

        GC.Collect();
        long managedMem6 = GC.GetTotalMemory(true);
        long unmanagedMem6 = MemoryUsage();

        Debug.Print("");
        Debug.Print("Testing {0}\\{1}", fgdbPath, className);
        Debug.Print("Num features found: {0}", featCount);
        Debug.Print("\t\t\t\t\tManaged (Kb)\t\tTotal (Kb)");
        Debug.Print("\t\t\t\t\t------------\t\t--------------");
        Debug.Print("Start\t\t\t\t{0}\t\t\t\t\t{1}", managedMem1 / 1024, unmanagedMem1 / 1024);
        Debug.Print("Features retrieved\t{0}\t\t\t\t\t{1}", managedMem2 / 1024, unmanagedMem2 / 1024);
        Debug.Print("Features nulled\t\t{0}\t\t\t\t\t{1}", managedMem3 / 1024, unmanagedMem3 / 1024);
        Debug.Print("GC Collected\t\t{0}\t\t\t\t\t{1}", managedMem4 / 1024, unmanagedMem4 / 1024);
        Debug.Print("Workspace released\t{0}\t\t\t\t\t{1}", managedMem5 / 1024, unmanagedMem5 / 1024);
        Debug.Print("Workspace collected\t{0}\t\t\t\t\t{1}", managedMem6 / 1024, unmanagedMem6 / 1024);
    }

    static List<IFeature> GetAllFeats(IFeatureClass fc, bool recycling)
    {
        List<IFeature> outList = new List<IFeature>();
        IFeatureCursor fCur = fc.Search(null, recycling);
        IFeature feat;
        while ((feat = fCur.NextFeature()) != null)
            outList.Add(feat);
        Marshal.FinalReleaseComObject(fCur);
        return outList;
    }

    static long MemoryUsage()
    {
        var prc = Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName);
        return prc[0].PrivateMemorySize;
    }

    static IWorkspace OpenFromPath(string workspacePath)
    {
        if (workspacePath.Substring(workspacePath.Length - 4) == ".gdb")
        {
            IWorkspaceFactory2 wsFactory = new FileGDBWorkspaceFactoryClass();
            return wsFactory.OpenFromFile(workspacePath, 0);
        }
        else
        {
            throw new NotSupportedException("No supported geodatabase type matches the provided workspace path: " + workspacePath);
        }

    }

Best Answer

When I run the code below 3 times for a file gdb polygon featureclass, I get the following:

Memory Before 951316
Memory After 1923476
Difference 972160
Feats loaded 44368
After collect 951364
Difference -972112

Memory Before 951320
Memory After 1923464
Difference 972144
Feats loaded 44368
After collect 951364
Difference -972100

Memory Before 951320
Memory After 1923464
Difference 972144
Feats loaded 44368
After collect 951352
Difference -972112

What do you get?

public static void MemoryTest(IApplication app)
{
    IFeatureLayer fLayer = ((IMxDocument)app.Document).FocusMap.get_Layer(0) as IFeatureLayer;
    long mem1 = GC.GetTotalMemory(true);
    Debug.Print("Memory Before {0}", mem1);
    List<IFeature> feats = GetAllFeats(fLayer.FeatureClass);
    long mem2 = GC.GetTotalMemory(true);
    Debug.Print("Memory After {0}", mem2);
    Debug.Print("Difference {0}", mem2 - mem1);
    Debug.Print("Feats loaded {0}", feats.Count);
    feats = null;
    GC.Collect();
    long mem3 = GC.GetTotalMemory(true);
    Debug.Print("After collect {0}", mem3);
    Debug.Print("Difference {0}", mem3 - mem2);
}
public static List<IFeature> GetAllFeats(IFeatureClass fc)
{
    List<IFeature> outList = new List<IFeature>();
    IFeatureCursor fCur = fc.Search(null, false);
    IFeature feat;
    while ((feat = fCur.NextFeature()) != null)
        outList.Add(feat);
    Marshal.FinalReleaseComObject(fCur);
    return outList;
}
Related Question