Graphical User Interfaces (GUIs) tend to be event driven. A user performs an action and the GUI sends that event to any interested components through a Model-View-Controller mechanism. When the events being sent represent fine grained actions (for example a single field on a form changed, as opposed to coarse grained events like the form being submitted), the performance of the GUI can become an issue. Other examples of when performance of the GUI might become an issue are:
- when the model in the GUI is large,
- when the GUI needs to process the data a lot in preparation for displaying it (either client or server side),
- when the GUI is poorly implemented and duplicate listeners exist meaning that components are refreshed multiple times, unnecessarily.
The following strategy has been used to help improve GUI performance. This list is ordered with the easiest / most important tasks (least effort, best improvement) at the top:
- ensure a clean MVC implementation, without duplicate listeners,
- add data caches,
- optimise caches (e.g. they hold more than one object graph),
- optimise refreshing of non-active elements.
1) Clean MVC Implementation
First of all, read the MVC article on this blog. Then, using a profiler or debugger follow your controller as it fires model change events. Are any model listeners fired more than once for any reason? If they are, it is less than optimal and you need to first look for mistakes in the programming logic. If there really is a good reason that the event is fired for a component multiple times, look at getting the component to cache events and process them all on the last event. This can get messy however, and you need to ask yourself if the event model is coarse grained enough. Why does a component get multiple events in the first place?
2) Add Data Caches
In a good implementation you should be using design patterns such as the Business Delegate. This pattern hides the details of where data is retrieved from. It might come from a service or maybe directly from a database. Its client doesn’t actually care. So this is the ideal place to start caching your model data, if not directly within the model (if you have one central model for your entire GUI). Either way, the aim of a cache is to only retrieve data from a slow method call if it’s really required. For example, the user saves a sales order. In order to get updated stock levels, you probably need to call a service which will take some time. In this case, as soon as the sales order is saved, the stock cache becomes dirty, meaning that it needs to be updated before it can be used safely. On the other hand, if a user adds a customer to the system, this has no effect on the stock levels, so there is no need to reload the stock cache. It is not marked as dirty.
Since this cache is reliant on listening to the model to determine if it is dirty or not, it needs to be client side. Whether it is stored in the business delegate or separately is the designers choice.
3) Cache Optimisation
Taking the above example of stock and sales, consider the following example. A user wants to add an item to a sales order. To do so, they need a list of products to choose from. That list of products comes from a products cache, however it might be dependent on the stock list, if they can only sell things which are in stock. As soon as they add the item to the sales order, the stock cache becomes dirty, since stock has been removed.
A cache of the stock in a warehouse might hold a lot of details about the stock. For example, each stock item might have a reference to its supplier which might be a fully loaded supplier object containing all the details of that supplier. It would probably be better to look up the supplier in a supplier cache, but that in turn is poor for performance. So reloading this stock cache and all its deep object graph (all the supplier info) might take a long time.
As soon as the user wants to sell something else from stock, the system needs that stock cache to be reloaded, because it is now dirty from the previous sale. But above it was just stated that that will take a long time. So let’s think about the data that is actually required. Do you need supplier information for selling stuff? Unlikely. So why load it all? A better option is to hold two caches. The first is the original with references to supplier information. The second is a light weight cache with a shallow object graph, meaning you simply have a list of stock items with as few details as required during sales. To load the list of products that are in stock is now much quicker. The trade off is that you hold the list of stock twice, but at least the second list is light weight.
4) Optimise Refreshing of Non-active Elements
Have a look at the GUI below:
Each tab is a different view of the overall model. Generally speaking, they all have a table which shows data in rows and a form which drills down to show the details of the selected row. But only the current tab needs to be refreshed if the relevant model change event comes along. Since all tabs inherit from a common class, that common class was changed to decide for itself if it would refresh straight away, or store the event and refresh later, when the view becomes active.
In fact, as well as remembering what to refresh and deferring until later, it also optimises what needs refreshing. First of all, it wouldn’t refresh the table twice! Second, there are several levels of refreshing. First, the table can be refreshed without reloading data from the cache (if for example the objects in its model contain the changes that need showing, which is what happens when the user updates data in the form). Second, the table can be refreshed after reloading its model from the cache. Finally, as well as reloading the table, it can change the selected row and update the form. If an event for this final refresh level comes in, while the view is inactive, there is no need to complete refreshes for the other two levels, since this final level already incorporates the other two. Same goes for the second level, in that it already includes the first level. So the code works out what really needs refreshing and defers until the point where you really need that refresh, namely when the view is activated. The trade off is that activating a view (by selecting its tab) is slower, but overall far fewer cache reloads and table/form refreshes take place.
This optimisation strategy was used on maxant BookStore. The results were a fully usable GUI that stores many thousand rows of data in memory. The GUI is quick and satisfying to use. Before the optimisations, the GUI was very painful to use indeed, and would easily have put off potential customers from buying the product.