Tuesday, January 8, 2008

Make your web application run faster

Introduction
It is easy to develop your own ASP.NET web application. But making it do some useful things for your users while keeping the design simple and elegant is not so easy. If you are lucky, your web application will be used by more than a handful of users, in that case, performance can become important. For some of the web applications I worked on, performance is vital: the company will lose money if users get frustrated with the slow response.

There are many factors that can result in bad performance, the number of users is just one of them. As a developer in a big corporation, you usually don't have a chance to mess with real production servers. However, I think it is very helpful for developers to take a look at the servers that are hosting their applications.

Your server spends most of its time waiting
Production servers usually host many applications. One of our web applications was not performing well, I suspect that other applications running on the server were using memory and CPU resources that "should" be devoted to our application. The admin allowed me to look at the server machine, what I found was not what I expected: the server had plenty of unused memory and the CPU usage was pretty low, too. It seems the server was idle most of the time.

That means if we design the application differently, we may be able to trade CPU and memory resources for better performance.

Application dependencies
It is typical for web applications to depend on many services running on remote servers. The slow response from those remote servers is likely the real cause of bad performance for a web application. For example, one of our web applications needs to request data from a remote server, a single request alone takes about 3 to 5 seconds. If my application has to make 5 to 7 different requests from remote servers in order to display a web page, then the performance will not be good even if only one user is using the application!

My approach for solving the performance problem was to design the application in a way that each page will make as few requests to remote services as possible. Which means the application will not make a remote request to a backend server until the data is really needed and once the data is retrieved, it will be cached within the application so that it doesn't need to request the same data more than once. This approach worked fine for us until ...

The management decided to change to a new design that would kill our application
What they want is a more user friendly interface. The first page will be designed in a way that as soon as a user landed on that page, he/she will see a summary of all the important information right away. If more detail is desired, the user can click tabs, links, or buttons on that page to display more data.

The problem is, information requested on the first page can only be extracted from data items returned by various remote service calls. There is no single service that can give us such a "summary" of the data.

So there is no choice but retrieving all the data items from remote servers before displaying the first page. The performance became so bad that even developers hated to use the application.

The Solution
Fortunately, our server has extra power to spare and the remote services we need do not depend on each other. After some research, I devised a new way to retrieve data from remote services. Previously, the sequence of steps to get data was as follows:

Step 1. If data item 1 is not in cache already, retrieve it by calling service 1 synchronously
Step 2. If data item 2 is not in cache already, retrieve it by calling service 2 synchronously
Step 3. If data item 3 is not in cache already, retrieve it by calling service 3 synchronously
My idea is, in step 1 while the application is retrieving data item 1, we also let it retrieve other data items in the background asynchronously (and cache the data items once they are received). By the time the app moves to step 2 and step 3, the data items will already be available in cache. Here is the new approach:

Step1. In this first step we do multiple things:
If data item 1 is not in cache already, retrieve it by calling service 1 synchronously
In addition, service calls for data item 2 and 3 are issued simultaneously and asynchronously if they are not in cache already
Data retrieved with the above asynchronous requests will be cached
Step 2. If data item 2 is not in cache already, retrieve it by calling service 2 synchronously
Step 3. If data item 3 is not in cache already, retrieve it by calling service 3 synchronously
Now, let's see the potential difference in performance. With the old approach, suppose it takes 5, 2, 3 seconds to retrieve data items 1, 2, 3, respectively, the total time will be at least 5+2+3 = 10 seconds. With the new approach, since we assume extra server power is available and the remote services are unrelated/independent, the ideal total time will be a little more than the longest of all data requests, which is 5 seconds in this example. So we can reduce the response time by almost 50%!

Let me explain the idea again in using plain English (no plain English compiler needed). Let's say you are ordering 3 dishes in a restaurant.

The old way: You order from the same waitress 3 times, each time the waitress will bring back a dish from the kitchen and put it on your table.
The new way: You order from three waitresses at once, they will be working simultaneously to bring three dishes from the kitchen, put the first dish on your table and the other two dishes on the table next to you. When you need the second and the third dish, a waitress will retrieve it from the next table and put it on your table, there is no need to go back into the kitchen again.
Assuming going back to the kitchen is the most time consuming work, we can save a significant amount of time with the new approach.

The Implementation
My solution is not worth much if I cannot get it to work and other people won't be interested if it is not implemented as a reusable component. Here is what I included with this article.

PerormanceEnhancer.dll
This is a DLL that can be reused in any ASP.NET application. It does not have any dependency other than the .NET framework.

In order to use this component to call remote services, you need to write at least one wrapper class with a default constructor and implement each remote service call in a public method within the wrapper class. Different types of remote service calls can be implemented in different wrapper classes or in different methods of the same wrapper class. PerformanceEnhancer.dll will be used to call the methods in the wrapper classes in exactly the same way described above, i.e. a mixture of synchronous and asynchronous calls. This component is also responsible for caching.

You use PerformanceEnhancer.dll by calling its only public static method, GetApplicationData. For each request to a remote service, you call GetApplicationData, you can optionally supply parameters to make other requests simultaneously and asynchronously. Here is the signature of this method:

public static object[] GetApplicationData
(
string sSessionID,
string sMethodName,
string sClassName,
string sAssemblyPath,
object[] pInput,
bool bUseCache,
bool bClearCache,
int nTimeout,
object[] pMoreRequests
)



Parameter Description
sSessionID, this is typically the ASP.NET session id, but you can use any string. It is mainly used to cache data
sMethodName, name of the method in a wrapper class that makes a remote service call
sClassName, full name (including namespace) of the wrapper class
sAssemblyPath, it can be either the full path or the file name of the assembly that contains the wrapper class. In case it is not the full path, the assembly must be located in the same folder as PerformanceEnhancer.dll
pInput, an object array that contains input parameter values for the method in the wrapper class. Each parameter can be of any type
bUseCache, boolean flag indicating whether to use cache
bClearCache, boolean flag indicating whether to clear cache before making a remote service call
nTimeout, timeout value in seconds
pMoreRequests, an object array while each item is an object array that contains parameters for an additional remote service call. The additional remote service calls will be made asynchronously
Return Value is an object array of two items:

The first item is the return value of the called method in the wrapper class, it can be of any type and it can be null
The second is an Exception object representing error occurred while calling the wrapper class method. It is null if there is no error
By checking the Exception object (null or not null) in the returned object array you can determine whether the remote service call is successful or not.

Here is what happens under the cover. If the pMoreRequests parameter is null, GetApplicationData will be behaving like a normal synchronous call, it returns when data is received from the remote server. When you specify parameters in pMoreRequests for additional service calls, then it will make those additional service calls asynchronously by utilizing the system thread pool: each additional service request will be queued as a user work item.

Demo Programs
The PETest.dll contains two wrapper classes for demo purposes. The methods in these classes simulate remote service calls that take specified number of seconds to complete. I also included an ASP.NET web application, PETestWeb, with only one simple page TestForm.aspx.

The TestForm.aspx page makes four requests (i.e. four remote service calls) by calling the two wrapper classes in PETest.dll. It displays the start time, the end time, and the times to make each remote service call.

The first request should take 10 seconds, the second 5 seconds, the third 8 seconds, and the fourth 8 seconds. If we do things the old way, then the total time it takes to complete the four requests will be 10+5+8+8 = 31 seconds. However, because we are using PerformanceEnhancer.dll, the total time is rarely more than 11 seconds. This is due to the fact that when the page makes the first request, it also starts to make other requests asynchronously and caches the data retrieved by the asynchronous calls. So the second, third, and fourth requests will find data in cache and return right away.

Here is a screen shot of the TestForm.aspx page in the demo app, PETestWeb:



As you can see, only the first request takes a little more than 10 seconds, the time to complete other requests can be ignored. If you refresh the page, the results will come back immediately because all the data items have been cached when the page is displayed for the first time. This is the best possible result, of course.

Some tips on using PerformanceEnhancer.dll
Here is something you need to know about this approach and this component.

If the requests you are trying to make depend on one another (you need to get data from one request in order to make another request), then you won't be able to get the ideal result as demonstrated above.
If the remote services you use depend on one another (say, they all consume some shared resources), then your performance may not be ideal either.
The basic idea is, you make synchronous remote service calls in the same sequence as before, except that while making the first synchronous call, you also issue asynchronous calls to request other data items simultaneously. Hopefully, the data will be available in cache at the time the other synchronous calls are made. PerformanceEnhancer.dll is a tool that makes doing this relatively easy.
It is best to make the call that takes the longest time first.
If the remote service calls you want to make do not take much time to begin with (say, they only take a fraction of a second), then using PerformanceEnhancer.dll is not going to help you, you may end up with worse performance.

Some Comments
Sometimes it is possible to solve hard performance problems by combining simple ideas and common sense with careful engineering. Although not rocket science, I think this article provides a good demonstration. Everyone knows about multithreading, but how to use it at the right places in your application is a totally different matter. Unfortunately, creative thinking is not always encouraged in big corporations, Political Correctness (i.e. Teamwork, Process, etc.) is more important than anything else. When I presented my solution, the response I got is "What? You wrote code without a meeting to discuss its design?" I am not even sure they are going to use my solution. Sigh.

License

No comments: