Udostępnij za pośrednictwem


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

Comments

  • Anonymous
    July 04, 2008
    PingBack from http://blog.a-foton.ru/2008/07/debugging-x-object-leaks/

  • Anonymous
    July 06, 2008
    The comment has been removed

  • Anonymous
    July 07, 2008
    There is no automatic garbage collection of interop code.  Just like when holding resources to any external system (SQL, Files, etc...), resources must be freed manually. In C#, you do not use a finalizer for the creator to destroy the object, you use the .Dispose() method.  By wrapping an object in a "using" statement, the Dispose call is automatically emitted.  Finalizers act as a final backstop if the developer forgets to call Dispose.  AX doesn't implement finalizers in BC, so you'll have to call Dispose to clean up those external resources.

  • Anonymous
    July 31, 2008
    i have created a structural solution for this issue. it is posted on my blog.

  • Anonymous
    October 02, 2008
    Hi,I would ask to you some help about a problem with our AOS.We have 3 AOS Dynamics AX 4.0 SP2 with 4GB each (and /3GB option active).During last month we have problem with memory used by process Ax32serv.exe, on each AOS RAM go up 2GB, and after 2.7GB the service crash. I think there's some memory leak, but I'm not able to find it.Do you have some link, idea, fix to sove this problem?We don't want to restart AOS every week, because one month ago we disn't have this problem.Thanks for your help,d.

  • Anonymous
    December 04, 2008
    We tried to use your xpo, but we have the impression that we only get information for the user currently logged on. Is this true? If yes, is there a way to dump the heap for all users?We do not use the business connector, so we are talking about x++ object leaks in ax.

  • Anonymous
    June 08, 2009
    You might got into this situation before... you are working in the Dynamics AX Client as usual and all

  • Anonymous
    January 10, 2010
    The image in this article does not show. Seems to be located in some Office Web Location which visitors do not have read access.Just a heads up. ;-)