1. Introduction
Performance
is often an afterthought. Many development teams rarely pay attention
to performance until late in the development process or, more
critically, after a customer reports severe performance problems in a
production environment. After a feature is implemented, making more
than minor performance improvements is often too difficult. But if you
know how to use the performance-optimization features in Dynamics AX,
you can create designs that allow for optimal performance within the
boundaries of the Dynamics AX development and runtime environments.
2. Client/Server Performance
Client/server
communication is one of the key areas of optimization for Dynamics AX.
In this section, we detail the best practices, patterns, and
programming techniques that yield optimal communication between the
client and the server.
2.1 Reducing Round-Trips Between the Client and the Server
The following three techniques can cover between 50 and 80 percent of round-trips in most scenarios:
CacheAddMethod
Display
and edit fields are used on forms to display data that must be derived
or calculated based on other information in the table. They can be
written on either the table or the form. By default, these fields are
calculated one by one, and if there is any need to go to the server
during one of these methods, as there usually is, each of these
functions goes to the server individually. These fields are
recalculated every time a refresh is triggered on the form, which can
originate from editing fields, using menu items, or the user requesting
a form refresh. Such a technique is expensive both from a
round-tripping perspective as well as in the number of calls it places
to the database from the Application Object Server (AOS).
For
display and edit fields declared in the form’s Data Source, no caching
can be performed because the fields need access to the form metadata.
If possible, you should move these methods to the table. For display
and edit fields declared on a table, you have to use FormDataSource.CacheAddMethod to enable caching. This method allows the
form’s engine to calculate all the necessary fields in one round-trip
to the server and then to cache the results from this call. To use cacheAddMethod, in the init method of a Data Source that uses display or edit methods, call cacheAddMethod on that Data Source, passing in the method string for the display or edit method. For an example of this, look at the SalesTable form’s SalesLine Data Source. In the init method, you find the following code.
public void init() {
super();
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, invoicedInTotal));
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, deliveredInTotal));
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, reservedPhysicalInSalesUnit));
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, reservedOnOrderInSalesUnit));
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, onOrderInSalesUnit));
salesLine_ds.cacheAddMethod(tablemethodstr(SalesLine, qualityOrderStatusDisplay));
}
|
If
this code is commented out, each of these display methods is computed
for every operation on the form Data Source, thus increasing the number
of round-trips to the server as well as the number of calls to the
database server.
For Dynamics AX 2009, Microsoft made a significant investment in the CacheAddMethod
infrastructure. In previous releases, this worked only for display
fields, and only on form load. In Dynamics AX 2009, the cache is used
for both display and edit fields, and is used throughout the lifetime
of the form, including reread, write, refresh, and any other method
that reloads the data behind the form. On all these methods, the fields
are refreshed, but the kernel now refreshes them all at once rather
than individually, as it did in previous versions of Dynamics AX.
RunBase Technique
RunBase classes form the basis for most business logic inside of Dynamics AX. RunBase
provides much of the basic functionality needed to perform a business
process, such as displaying a dialog, running the business logic, and
running the business logic in batches. When business logic executes
through RunBase, the logic flows as shown in Figure 1.
Most of the round-trip problems of RunBase originate with the dialog. The RunBase
class should be running on the server for security reasons as well as
for the fact that it is accessing lots of data from the database and
writing it back. A problem occurs when the RunBase class itself is marked to run on the server. When the RunBase
class is running on the server, the dialog is created and driven from
the server, causing an immense number of round-trips between the client
and the server.
To avoid excessive round-trips, mark the RunBase class to run on Called From, meaning that it will run on either tier, and then mark either the construct method for the RunBase class or the menu item to run on the server. Called From enables the RunBase
framework to marshal the class back and forth between the client and
the server without having to drive the dialog from the server,
significantly reducing the round-trips needed across the application.
Keep in mind that you must implement the pack and unpack methods in a way that allows this serialization to happen.
For an example of the RunBase class, examine the SalesFormLetter class in the base application. For an in-depth guide to implementing RunBase to optimally handle round-trips between the client and the server, refer to the Microsoft Dynamics AX 2009 White Paper, “RunBase Patterns,” which you can find at the Microsoft Download Center.
Caching and Indexing
Dynamics
AX has a data caching framework on the client that can help you greatly
reduce the number of times the client goes to the server. In previous
releases of Dynamics AX, this cache operated only on primary keys. In
Dynamics AX 2009, this cache has been moved and now operates across all
the unique keys in a table. Therefore, if a piece of code is accessing
data from the client, the code should use a unique key if possible.
Also, you need to ensure that all keys that are unique are marked as
such in the Application Object Tree (AOT). You can use the Best
Practices tool to ensure that all your tables have a primary key.
Properly setting the CacheLookup property is a prerequisite for using the cache on the client. Table 1 shows the values that CacheLookup can have.
Table 1. Table Cache Definitions
Cache Setting | Description |
---|
Found | If
a table is accessed by a primary key or a unique index, the value is
cached for the duration of the session or until the record is updated.
If another AOS updates this record, all AOSs will flush their cache.
This cache setting is appropriate for master data. |
NotInTTS | Same as Found
except every time a transaction is started, the cache is flushed and
the query goes to the database. This cache setting is appropriate for
transactional tables. |
FoundAndEmpty | Same as Found
except if the query fails to find a record, the absence of the record
is stored. This cache setting is appropriate for region-specific master
data or master data that isn’t always present. |
EntireTable | The
entire table is cached in memory on the AOS, and the client treats this
cache as “Found.” This cache setting is appropriate for tables with a
known number of limited records, such as parameter tables. |
None | No
caching occurs. This setting is appropriate in only a few cases, such
as when optimistic concurrency control has to be disabled. |
When caching is set, the client stores up to 100 records per table, and the AOS stores up to 2000 records per table.
Index caching works only if the where clause has column names that are unique. In other words, caching won’t work if a join
is present, if the query is a cross-company query, or if any range
operations are in the query. Therefore, if you’re checking whether a
query record that has a particular primary key and some other attribute
exists, search the database only by primary key. For an example of
this, refer to xDataArea.isVirtualCompany.
static boolean isVirtualCompany(DataAreaId dataAreaId) { DataArea dataArea1;
boolean fRetVal; ; fRetVal=FALSE; select Id,isVirtual from dataArea1 where dataArea1.Id == dataAreaId; if(dataArea1.Id && dataArea1.isVirtual==1) { fRetVal=TRUE; } return fRetVal; }
|
Notice
that this code queries the database by ID and then the primary key, and
then it checks the virtual company in memory. This operates at around
the same speed but allows the query to hit the cache and the result of
the query to be cached on both the client and server tiers.
EntireTable
caches store the entire contents of a table on the server, but the
cache is treated as a “Found” cache on the client. For tables that have
only one row per company, such as parameter tables, you should add a
key column that always has a known value, such as 0. This allows the
client to use the cache when accessing these tables. An example of the
use of a key column in the base application (i.e., Dynamics AX 2009
without any customizations) is CustParameters table.
2.2 Writing Tier-Aware Code
When
you’re writing code, you should be aware of what tier the code is going
to run on and what tier the objects you’re accessing are on. Objects
that have their RunOn property set to Server/Client/Called From are always instantiated on the server. Objects that are marked to RunOn Client are always instantiated on the client, and Called From is instantiated wherever the class is created. One caveat: if you mark classes to RunOn either the client or the server, you can’t serialize them to another tier via pack and unpack.
If you attempt to serialize a server class to the client, all you get
is a new object on the server with the same values. Static methods run
on whatever tier they are specified to run on via the Client, Server, or Client Server keyword in the declaration.
Working with Temp Tables
Temp
tables can be a common source of both client callbacks and calls to the
server. Unlike regular table buffers, temp tables reside on the tier on
which the first record was inserted. For example, if a temp table is
declared on the server, the first record is inserted on the client, and
the rest of the records are inserted on the server, all access to that
table from the server happens on the client. It’s best to populate a
temp table on the server because the data you need is probably coming
from the database; still, you must be careful when your intent is to
iterate over the data to populate a form. The easiest way to achieve
this efficiently is to populate the temp table on the server, serialize
the entire table down to a container, and then read all the records
from the container back into a temp table on the client.
Removing Client Callbacks
Client
callbacks occur when the client places a call to a server-bound method,
and then the server places a call to a client-bound method. These calls
can happen for two reasons. First, they occur when the client doesn’t
send enough information to the server during its call, or sends
the server a client object that encapsulates the information. Second,
client callbacks occur when the server is either updating or accessing
a form.
To remove the first kind of call,
ensure that you send all the information the server needs in a
serializable format, such as a packed container, record buffers, or
value types (e.g., int, str, real, boolean). When the server accesses these types, it doesn’t need to go back to the client, as it does if you use an object type.
To
remove the form’s logic, just send any necessary information about the
form into the method, and manipulate the form only when the call
returns instead of directly from inside the server. One of the best
ways to defer operations on the client is with pack and unpack. By utilizing pack and unpack, you can serialize a class down to a container and then deserialize it on the other side.
Chunking Calls
To
ensure the minimum number of round-trips between the client and the
server, chunk them into one, static server method and pass in all the
state needed to perform the operation.
One static server method you can use is NumberSeq::getNextNumForRefParmId. This method call is a static server call that contains the following line of code.
return NumberSeq::newGetNum(CompanyInfo::numRefParmId()).num();
|
Had this code run on the client, it would have caused four remote procedure call (RPC) round-trips (one for newGetNum, one for numRefParmId, one for num, and one to clean up the NumberSeq object that was created). By using a static server method, you can complete this operation in one RPC round-trip.
Another
common example of chunking occurs when the client is doing Transaction
Tracking System (TTS) operations. Frequently, a developer writes code
similar to the following.
Ttsbegin; Record.update(); TTSCommit
|
You
can save two round-trips if you chunk this code into one static server
call. All TTS operations are initiated only on the server. To take
advantage of this fact, do not invoke the ttsbegin and ttscommit call from the client to start the database transaction when the ttslevel is 0.