Garbage Collection and RPC calls in X++


Dynamics AX is a 3-tier application that evolved from a 2-tier application.  Yes, this is right, the first versions of Axapta was solely a client side application communicating with the database. In version 2.0 the middle tier was introduced. The X++ language got a few new keywords, client and server, and the AX run-time provided the smartest marshaling of objects across the tiers on the planet. The marshaling is guaranteed to work in virtually any object graph you can instantiate. You can have client-side classes holding references to instances of server-side classes, which contains references back to other client-side objects, which references … you get the idea. All you have to do as a developer is to decorate your classes as client, server or called-from.

You don’t have to worry about any low level details like how the instances are communicating across the wire. The key word in the previous sentence is “have” – stuff will just work, but unless you are very careful you may end up creating an extremely chatty (i.e. lot of RPC calls on the wire) implementation.  Recently I’ve seen two cases of well-intended changes that on the surface looked right, but both caused an explosion of RPC calls. Both were implemented by smart guys, and I wanted to point them to an article explaining the problem – and I realized that article didn’t exist. Until now.

Garbage collection in AX

The garbage collector (GC) AX is responsible for releasing memory consumed by object instances no longer in use. In .NET the GC is indeterministic, it runs when it “feels like” running, typically when the system has CPU capacity and is low on memory.   In contrast the GC in AX is deterministic – it runs every time an object goes out of scope.

Consider this small example:

static void GCJob1(Args _args)
{
    MyServerClass myServerClass;
    //Create instance
    myServerClass = new MyServerClass();
    //Release instance
}

image

Jobs run on the client tier, so this will create an instance of the MyServerClass and release it again. MyServerClass is a trivial class with RunOn=Server.

If we enable Client/Server trace under Tools | Options | Development, and run the job, we get:

Create instance
Call Server: object: MyServerClass.new()
Release instance
Call Server: destruct class

Notice this: The client-tier reference to the server instance, is keeping the instance alive. When the reference goes out-of-scope, then the GC takes over and calls the server to free the server memory.

Island detection

The GC is not just looking for instances without references – often object graphs are more complicated. To release memory that is no longer needed, the GC is looking for groups of objects without any external references – or in popular lingo: Islands. This search is potentially harmful to the performance of your application. The GC must traverse all members of any object that goes out of scope – regardless of their tier.

Let’s build out the example by introducing a client side class that is referenced by the server instance.

static void GCJob2(Args _args)
{
    MyServerClass myServerClass;
    MyClientClass myClientClass;
    //Create client instance
    myClientClass = new MyClientClass();
    //Create server instance
    myServerClass = new MyServerClass();
    //Make server instance reference client instance
    myServerClass.parmMyClientClass(myClientClass);
    //Release instances
}

image
Now; when myServerClass goes out-of-scope then the GC will start analyzing the object graph, and it will discover an island consisting of our two objects – despite they are on different tiers, and it will release the memory consumed.
image

Pretty smart – but not for free!

 

This is the resulting RPC traffic from the above job:

Create client instance
Create server instance
Call Server: object: MyServerClass.new()
Make server instance reference client instance
Call Server: object: MyServerClass.parmMyClientClass()
Call Client: set class loop dependencies
Call Client: test class loop dependencies
     
Release instances
Call Server: destruct class
Call Client: destruct class

Now suddenly we jumped from 2 RPC calls to 6! What happened? We met the GC! 

  • The first 2 calls are expected, they are a direct consequence of the methods invoked on the server tier.
  • The next 2 calls are set/test class loop dependencies. Both of these are consequences of the parm method. The set call is a result of the assignment inside the parm method. It tells the client that the server now holds a reference to the client side object. The test call is the GC looking for islands, but not finding any. When the parameter (containing the client-side object) goes out-of-scope at the end of the parm method, then GC looks for islands. As the server side class holds a client side member, then the traversal of the object graph requires a trip to the client.  
  • The last 2 calls are cleaning up. Notice that destruction is a chain reaction. First the server is called to destruct the server side object, then the server calls back to the client to destruct the client-side object.

 

A real life example

Consider a server side class that is looping over some data, and for each, say, row, it spins up another class instance on the server to do some calculations.  This is all server side, and perfect. So let’s add a client-side member to the mix.

class MyServerClass
{
      MyClientClass myClientClass;

      public void run()
      {
          int i;
          MyServerHelper myServerHelper;
          //Create client-side member
          myClientClass = new MyClientClass();

          //Loop over some data
          for (i=1; i<=10; i++)
          {
              myServerHelper = new MyServerHelper();
              myServerHelper.parmMyServerClass(this);
              myServerHelper.calc();
          }
      }
  }

image

The alarming result is 10 client calls – or one per iteration in the loop – despite the loop only contains server side logic.

Create client-side member
Call Client: object: MyClientClass.new()
Call Client: set class loop dependencies
Loop over some data
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies
Call Client: set class loop dependencies

The assignment inside the parm method forces the AX runtime to traverse the object graph, and the object graph contains a client side instance.

The alert reader would have recognized this as the Runbase pattern. The client side class is the operation progress bar. In Dynamics AX 2009 the operation progress bar framework regressed, as a client side reference was introduced - exposing all thousands of consumers to this specific problem. This got fixed in Dynamics AX 2012 R3.

Symmetrical implementation

The implementation of the GC and the supporting runtime is symmetrical on each tier – you can recognize them in action, when you come across these calls in the Trace Parser. Remember; they are always a consequence of the exercised X++ logic. I.e. something that can be addressed if required.

Call Client: set class loop dependencies
Call Client: test class loop dependencies
Call Client: destruct class

Call Server: set class loop dependencies
Call Server: test class loop dependencies
Call Server: destruct class

 

Wrapping up

There is only one way of understanding the impact the GC has on your implementation: Measure it!   The best tool for measurement is the Trace Parser. Alternatively, the Client/Server trace in Tools | Options | Development can be used – it will show all the RPC calls in the Message Window.

The rule-of-thumb as a developer is to avoid class members that are living on the opposite tier. This will ensure your object graphs are single tiered, and it will make the life of the runtime and the GC much simpler, and your applications equally faster.

There are situations where cross tier members seem unavoidable. However, there are techniques to avoid them, and achieve the same functional results. Take a look in the SysOperationProgress class in AX4 or AX 2012 R3 for an example.

 

Code samples are attached. They are provided AS-IS and confers no rights.

PrivateProject_GCProject.xpo

Comments (6)

  1. Kenny Saelen says:

    Recently, a customer with performance issues could not identify big query issues and no big batch processes were running. They only experienced slower forms, etc... and guess what: They had lots of RPC calls going on. So thanks for posts like these, they really explain some inner details that are unknown to many!

  2. Thanks for letting me know Diego.  I just updated the xpo - please try again.

  3. Diego Araujo says:

    Have a problem when i try get the XPO.

    I download it, but him are blank (0 bytes)

  4. Tommy Skaue says:

    Thanks for this, Michael. I love these posts from the people who really knows the inner details on what is happening under the hood in AX. Keep them coming, please. 🙂

  5. Martin Dráb says:

    Excellent post! Many developers don't take RPC calls seriously enough, because it's usual not one huge problem (like a slow query), it's rather something slowing the system down below the radar.

    Although I'm very careful about client/server calls, I usually don't think about GC. Thank you for pointing it out.

Skip to main content