JSF Performance Improvements: Part I

October 18, 2008

This is my first post on improving sites utilizing JSF. This post is more relevant for mostly static content that only changes at the result of database updates. One of the easiest ways to improve performance is through caching. There are several types of caching from database caching (JPA/Hibernate), model caching (EHCache), browser caching (resources, images, etc). The method I will discuss is creating a page cache within JSF. Note that this useful only when using a standard server and do not have access to higher end equipment such as routers that can automatically provide a page caching layer. If that equipment is available, those layers should always be used as they are built for that purpose. However, for simple sites looking for millisecond access times, this can be a pretty easy implementation.

To start, a high level overview. Whenever any JSF page is requested, we will check the cache, and if not available, we will delegate to JSF while also saving to cache. If it is in cache, we will merely use that file rather than going through the JSF lifecycle. HTTP servers are very fast/efficient at serving static resources, so you should easily see millisecond access times.


With the high level overview out of the way, let’s dig into the in-depth discussion. In order to handling the cache requests and page requests, we will use a filter. Let’s name the filter JsfCachingFilter. To use this filter, let’s add the filter to our web.xml file.


<filter>
<filter-name>JsfCachingFilter</filter-name>
<filter-class>com.znet.filter.JsfCachingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>JsfCachingFilter</filter-name>
<servlet-name>FacesServlet</servlet-name>
</filter-mapping>

With the filter defined, we can now intercept JSF requests. The first thing we must do is check if the current request is already cached. We do this by maintaining a set of request URLs to temporary file stores. We obtain the request using the request servlet path and the query parameters. We must use the query parameters to ensure we provide the unique path as different cache results could occur between query paramters. If the current map contains the specified key and the associated file is still valid, then we immediately serve the file as a static resource.


String method = request.getMethod();
if ("GET".equals(method))
{
String fullPath = this.getRequestPath(request);
if (this._cache.containsKey(fullPath))
{
String resourcePath = this._cache.get(fullPath);
File file = new File(this.getAbsolutePath(resourcePath));
if (file.exists())
{
String relativePath = this.getRelativePath(resourcePath);
request.getRequestDispatcher(relativePath)
.include(request, response);

return true;
}
}
}

Note that we only handle GET requests as POST requests usually require interaction with action methods. If the associated request has not yet been cached, then we must handle the request by both processing the JSF invocation as well as writing to a file to cache. We do this by first creating a random temporary file and caching the request path to the temporary file. The file is then processed by creating a response wrapper that uses a custom servlet output stream that uses the standard stream from the current response and a FileOutputStream. Thus, when any write action occurs on the wrapped stream, it delegates to both the underlying response stream as well as the file. As a result, we get the contents cached to the file for later usage and we output the data to the associated client connection.

Using this methodology, we can now go from long access time to quick access times. The part not discussed here is what do you do when your pages dynamic data changes. As I originally stated, this method only works with semi-dynamic data where you can know and handle the changes. For example, if you have an administration site, you can cause an expiration to the cache so that pages refresh themselves. If you have a DB based system, you can use a trigger to invoke a network expiration. However, if your data is from a system such as a network request or web service where your data modifications are indeterminate, then this method will not work (unless you have a way of checking modification access). The expirations can either occur by simplying flipping a switch that causes cached items with dates before the switch to automatically reprocess (if your pages offer semi quick access times without cache). You could also have a background thread that polls the expiration switch and then automatically updates the cache in the background in order to maintain high cache hit ratios (which is the expectation of any cache layer).

Here are some other key points to consider when implementing such a caching layer. First, if your JSF implementation supports using a “jsessionid” query parameter to maintain sessions without cookie support, you need to make sure to strip off the parameter when processing the servlet path. Otherwise, you would end up caching the same page for every user.

On the topic of users, it should also be noted that this layer will not cache properly if your pages use personalization, cookies, etc. This is due to the fact that the page is cached a single time for a user and if each user should have unique data, then it will be invalid as they will all be processed with same page data. One way around this is to use AJAX to retrieve the personalized data on page load.

Here are some final notes to consider. This implementation uses a disk cache to store files and use the servlet container to serve the static resources. However, if you really want fast performance and have plenty of RAM, you can use a memory cache to store data in RAM. If you have limited disk space, you can use automatic expirations through either a TTL based approach or through a LRU-based cache with a fixed size. Thus, if a particular page is too old in the cache, it will be removed in order to allow other pages to be cached. If a fixed size is used, then we can remove older entries automatically in order to ensure that the file never grows too large.

Comments are closed.