Debugging X++ Object Leaks

One of the most important aspects of writing managed code that interacts with AX through the Business Connector is cleaning up objects. Each AxaptaObject and AxaptaTable must have dispose called on it before going out of scope, or we’ll leak the object on the server with no way to clean it up. AxaptaObject and AxaptaTable do not implement Finalizers, so the onus is on the consumer of these APIs to do proper cleanup. If a session logs off, all of its objects are cleaned up regardless of whether or not they were disposed of properly, but if we have a long running session that isn’t properly disposing objects, we can quickly run the server out of memory causing it to crash. These problems can be notoriously hard to debug since there is no leaking happening on the Business Connector application, and there is little insight into what’s going on inside of the X++ runtime. We’ve also seen problems occur similar to this from straight X++ code when users put things into the GlobalCache and never take them out or continuously add to the global cache.

 

X++ does have an API to enumerate all objects on the Heap, and a form to do so as well. The form is called “SysHeapCheck” and can only be opened up from the AOT. Clicking the “Update” button will populate the current tab. This form, however, is only accessible from the Rich Client and does little to aggregate objects together, so it’s hard to visualize which, if any, objects are leaking. The other big issue with this form is that it only shows the objects that are alive on the Client, not those that are alive on the server, so if we’ve leaked a bunch of objects on the server this form won’t help us.

Figure 1: SySHeapCheck form

  image

This information is retrieved from the API HeapCheck inside of the X++ Runtime. We can use this API to provide a better abstraction of this information and make it accessible from inside of X++ code. I’ve created a useful utility called Heap Dump. You can find it attached to this post.

 

Let’s take a look at how we’ll use this code. The Main method provides a sample of this API:

static void Main(Args _args)

{

    Map result;

    MapEnumerator enum;

    str objectName, objectType;

    boolean runningOnClient;

    int aliveCount;

    int totalObjects;

    int totalCursors;

    container _map;

    ;

    [totalObjects, totalCursors, _map] = HeapDump::DumpAllObjects();

    result = Map::create(_map);

    enum = result.getEnumerator();

    info(strfmt("Total Objects: %1", totalObjects));

    info(strfmt("Total Cursors: %1", totalCursors));

    while(enum.moveNext())

    {

        [objectName, runningOnClient, objectType] = enum.currentKey();

        aliveCount = enum.currentValue();

        info(strfmt("%1 %2 %3 %4", objectName, runningOnClient ? "Client" : "Server", objectType, aliveCount));

    }

}

The DumpAllObjects returns a Container in the format of [<Total number of unfreed Objects>, <Total number of unfreed Cursors>, <Unfreed Object Map>]. The third parameter, the Map of Unfreed Objects, is of type Map(Types::Container, Types::Integer). The key to this map is a container in the format [<Type Name>, <1 if object is on Client, 0 if on the server>, <Cursor or Object>]. This implantation is a bit hairy, but it was done this way to maximize interoperability with .NET. If we’re in X++ and we’re looking at a leak from the rich client, we can just use the main method here.

We can call this from managed code to get the same information, as seen below.

The best way to avoid these problems to begin with is to use the “using” statement. You can find multiple examples of this in my code below. Every time I call a method that returns an AxaptaObject, AxaptaContainer, or AxaptaCursor, it’s wrapped in a using statement. By doing so, as soon as the object goes out of scope it gets cleaned up, and its memory freed.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Microsoft.Dynamics.BusinessConnectorNet;

namespace HeapDumpExample

{

    static class LambdaHelpers

    {

        /// <summary>

        /// Performs an action on each item in a list, used to shortcut a "foreach" loop

        /// </summary>

        /// <typeparam name="T">Type contained in List</typeparam>

        /// <param name="collection">List to enumerate over</param>

        /// <param name="action">Lambda Function to be performed on all elements in List</param>

        static void ForEach<T>(this IList<T> collection, Action<T> action)

        {

            for (int i = 0; i < collection.Count; i++)

            {

                action(collection[i]);

            }

        }

    }

    struct HeapDumpEntry

    {

        public string TypeName {get; set;}

        public bool IsRunningOnClient { get; set; }

        public string ObjectType { get; set; }

        public int AliveObjectCount { get; set; }

    }

    /// <summary>

    /// Helper class to allow us to perform LINQ queries over the X++ Map returned from the HeapDump call

    /// </summary>

    class EnumerableAxaptaMap : IEnumerable<HeapDumpEntry>

    {

        private AxaptaObject mMap;

        public AxaptaObject Map

        {

            get { return mMap; }

            set

            {

                if (startedEnumerating)

                {

                    throw new Exception("Can't change contianer once enumeration has started");

                }

                mMap = value;

            }

        }

        bool startedEnumerating = false;

        #region IEnumerable<HeapDumpEntry> Members

        public IEnumerator<HeapDumpEntry> GetEnumerator()

        {

            startedEnumerating = true;

            //Get an enumerator over the Map

            using (var MapEnumerator = Map.Call("getEnumerator") as AxaptaObject)

            {

  //Enumerate over the Map

                while ((Boolean)MapEnumerator.Call("moveNext"))

                {

                    using (var key = MapEnumerator.Call("currentKey") as AxaptaContainer)

                    {

                        var value = (Int32)MapEnumerator.Call("currentValue");

                        //The key contains a container with Type and location iformation, the value contains the number of objects alive

                        yield return new HeapDumpEntry

                        {

                            TypeName = key.get_Item(1).ToString(),

                            IsRunningOnClient = Convert.ToBoolean(key.get_Item(2)),

                            ObjectType = key.get_Item(3).ToString(),

                            AliveObjectCount = value

                        };

                    }

                }

                yield break;

            }

        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

        {

            //Lazy default implementation

            foreach (var x in this.ToList()) { yield return x; }

            yield break;

        }

       

        #endregion

    }

    class Program

    {

        static void Main(string[] args)

        {

            int TotalNumObjects, TotalNumCursors;

            List<HeapDumpEntry> AliveObjects;

            var CreatedObjects = new List<AxaptaObject>();

            using (Axapta session = new Axapta())

            {

                //Log onto AX

                session.Logon("", "", "", "");

                //Dump all objects created on Logon

                Console.WriteLine("BaseLine");

                WriteAliveObjectsToConsole(session, out TotalNumObjects, out TotalNumCursors, out AliveObjects);

                //Create Lots of Objects and don't dispose them

                for (int i = 0; i < 100; i++)

                {

                    CreatedObjects.Add(session.CreateAxaptaObject("Activities"));

                }

                //Dump current list of objects

                Console.WriteLine("Before Disposing Objects");

                WriteAliveObjectsToConsole(session, out TotalNumObjects, out TotalNumCursors, out AliveObjects);

                //Dispose all of the objects

                CreatedObjects.ForEach(obj => obj.Dispose());

                Console.WriteLine("After Disposing Objects");

                //Dump current list of objects again

                WriteAliveObjectsToConsole(session, out TotalNumObjects, out TotalNumCursors, out AliveObjects);

            }

        }

        private static void WriteAliveObjectsToConsole(Axapta session, out int TotalNumObjects, out int TotalNumCursors, out List<HeapDumpEntry> AliveObjects)

        {

            DumpObjects(session, out TotalNumObjects, out TotalNumCursors, out AliveObjects);

            Console.WriteLine("Total Number of Live Class Objects: {0}", TotalNumObjects);

            Console.WriteLine("Total Number of Live Cursors: {0}", TotalNumCursors);

            AliveObjects.ForEach(entry => Console.WriteLine("\t{0} {1} {2} {3}", entry.TypeName

                                                                               , entry.IsRunningOnClient ? "Client" : "Server"

                                                                               , entry.ObjectType

                               , entry.AliveObjectCount));

        }

        private static void DumpObjects(Axapta session, out int TotalNumObjects, out int TotalNumCursors, out List<HeapDumpEntry> AliveObjects)

        {

            TotalNumObjects = 0;

            TotalNumCursors = 0;

            AliveObjects = null;

            using (var objectsConatiner = session.CallStaticClassMethod("HeapDump", "DumpAllObjects") as AxaptaContainer)

            {

                if (objectsConatiner != null)

                {

                    TotalNumObjects = (Int32)objectsConatiner.get_Item(1);

                    TotalNumCursors = (Int32)objectsConatiner.get_Item(2);

                    using (AxaptaObject ObjectMap = session.CallStaticClassMethod("Map", "create", objectsConatiner.get_Item(3)) as AxaptaObject)

                    {

                        var MapEnumerable = new EnumerableAxaptaMap { Map = ObjectMap };

                        //We use an extension method to sort the list and put it to a list

                        AliveObjects = MapEnumerable.OrderByDescending(x => x.AliveObjectCount).ToList();

                    }

                }

            }

        }

    }

}

 

 

Class_HeapDump.xpo