RichFaces is too greedy


For my first post, let’s talk a little bit about RichFaces.

RichFaces is a very powerful JSF library which allows you to easily integrate Ajax capabilities into your website. To see all the components RichFaces has to offer, please click on the following link: http://livedemo.exadel.com/richfaces-demo/richfaces/actionparam.jsf

I am now using this framework for about two years and I quite like how easy it is to use! ๐Ÿ™‚ However, at the start, I was only using it in small projects with less than two hundred of unique visitors a day.

That is why I didn’t discover earlier that RichFaces was using so much memory! Indeed, I am now working on a project that has nearly 50,000 unique visitors a day. When we decided to rebuild it using JSF and RichFaces, I didn’t think that 6Gb of RAM wouldn’t be enough to keep it running! ๐Ÿ˜ฎ

People who knows JSF would say: “Why don’t you use ‘client’ as state saving method?”. Well, because of the complexity of my pages, the size of these pages increases so much that it can easily reach 2Mb (which is not acceptable). So, I need to keep the view state on server side.

Still in JSF, there is a way of compressing the view state before saving it in session (part of the web.xml file):

<context-param>
    <param-name>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</param-name>
    <param-value>true</param-value>
</context-param>

<context-param>
    <param-name>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</param-name>
    <param-value>true</param-value>
</context-param>

Strangely enough, the second parameter (COMPRESS_STATE_IN_SESSION) doesn’t work with RichFaces. After investigating the code, it seems that the RichFaces state manager (org.ajax4jsf.application.AjaxStateManager) doesn’t support compression at all! ๐Ÿ™

Why not? Well, I didn’t find a good reason so I decided to compress it myself. ๐Ÿ˜‰ The solution is to extend the AjaxStateManager and compress the view state before saving it in session and uncompress it when retrieving it from the session. Pretty easy!!! ๐Ÿ™‚

Enough talking, here is the code of this custom state manager:

package org.ajax4jsf.application;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.faces.FacesException;
import javax.faces.application.StateManager;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.render.ResponseStateManager;

import org.ajax4jsf.context.ContextInitParameters;

/**
 * Overrides the Ajax state manager to enable the state compression.
 * @author Stรฉphane Moreau
 */
public class MyAjaxStateManager extends AjaxStateManager {
	/**
	 * Only applicable if state saving method is "server" (= default) and if
	 * <code>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</code> is
	 * <code>true</code> (= default).
	 * If <code>true</code> (default) the serialized state will be compressed before
	 * it is written to the session.
	 * If <code>false</code> the state will not be compressed.
	 */
	private static final String COMPRESS_SERVER_STATE_PARAM = "org.apache.myfaces.COMPRESS_STATE_IN_SESSION";

	/**
	 * Default value for <code>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</code>
	 * context parameter.
	 */
	private static final boolean DEFAULT_COMPRESS_SERVER_STATE_PARAM = true;

	private static final int UNCOMPRESSED_FLAG = 0;
	private static final int COMPRESSED_FLAG = 1;

	private final ComponentsLoader componentLoader;

	public MyAjaxStateManager(StateManager stateManager) {
		super(stateManager);
		componentLoader = new ComponentsLoaderImpl();
	}

	/**
	 * Reads the value of the <code>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</code>
	 * context parameter.
	 * @see COMPRESS_SERVER_STATE_PARAM
	 * @param context <code>FacesContext</code> for the request we are processing.
	 * @return boolean true, if the server state steam should be compressed
	 */
	protected static boolean isCompressStateInSession(FacesContext context) {
		String value = context.getExternalContext().getInitParameter(
				COMPRESS_SERVER_STATE_PARAM);
		boolean compress = DEFAULT_COMPRESS_SERVER_STATE_PARAM;
		if (value != null) {
			compress = Boolean.valueOf(value);
		}
		return compress;
	}

	@Override
	protected Object[] buildViewState(FacesContext context) {
		Object[] viewStateArray = null;
		UIViewRoot viewRoot = context.getViewRoot();
		if (null != viewRoot && !viewRoot.isTransient()) {
			TreeStructureNode treeStructure = (TreeStructureNode) getTreeStructureToSave(context);
			Object state = getComponentStateToSave(context);
			if (isSavingStateInClient(context)) {
				viewStateArray = new Object[]{treeStructure, state};
			} else {
				viewStateArray = saveStateInSession(context, treeStructure,
						handleSaveState(context, state));
			}

		}
		return viewStateArray;
	}

	@Override
	public UIViewRoot restoreView(FacesContext context, String viewId,
			String renderKitId) {
		UIViewRoot viewRoot = null;
		ResponseStateManager responseStateManager = getRenderKit(context,
				renderKitId).getResponseStateManager();
		TreeStructureNode treeStructure = null;
		Object[] state = null;
		Object[] serializedView = null;
		if (isSavingStateInClient(context)) {
			serializedView = (Object[]) responseStateManager.getState(context,
					viewId);

			if (null != serializedView) {
				treeStructure = (TreeStructureNode) serializedView[0];
				state = (Object[]) serializedView[1];
			}
		} else {
			serializedView = restoreStateFromSession(context, viewId,
					renderKitId);

			if (null != serializedView) {
				treeStructure = (TreeStructureNode) serializedView[0];
				state = (Object[]) handleRestoreState(context, serializedView[1]);
			}
		}

		if (null != treeStructure) {
			viewRoot = (UIViewRoot) treeStructure.restore(componentLoader);
			if (null != viewRoot && null != state) {
				viewRoot.processRestoreState(context, state[0]);
				restoreAdditionalState(context, state[1]);
			}
		}
		return viewRoot;

	}

	private static final Object handleSaveState(FacesContext context, Object state) {
		if (ContextInitParameters.isSerializeServerState(context)) {
			ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
			ObjectOutputStream oas = null;
			try {
				OutputStream os = baos;
				if(isCompressStateInSession(context))
				{
					os.write(COMPRESSED_FLAG);
					os = new GZIPOutputStream(os, 1024);
				}
				else
				{
					os.write(UNCOMPRESSED_FLAG);
				}

				oas = new ObjectOutputStream(os);
				oas.writeObject(state);
				oas.flush();
			} catch (Exception e) {
				throw new FacesException(e);
			} finally {
				if (oas != null) {
					try {
						oas.close();
					} catch (IOException ignored) { }
				}
			}
			return baos.toByteArray();
		} else {
			return state;
		}
	}

	private static final Map<String,Class<?>> PRIMITIVE_CLASSES =
		new HashMap<String,Class<?>>(9, 1.0F);

	static {
		PRIMITIVE_CLASSES.put("boolean", boolean.class);
		PRIMITIVE_CLASSES.put("byte", byte.class);
		PRIMITIVE_CLASSES.put("char", char.class);
		PRIMITIVE_CLASSES.put("short", short.class);
		PRIMITIVE_CLASSES.put("int", int.class);
		PRIMITIVE_CLASSES.put("long", long.class);
		PRIMITIVE_CLASSES.put("float", float.class);
		PRIMITIVE_CLASSES.put("double", double.class);
		PRIMITIVE_CLASSES.put("void", void.class);
	}

	private static final Object handleRestoreState(FacesContext context, Object state) {
		if (ContextInitParameters.isSerializeServerState(context)) {
			ObjectInputStream ois = null;
			try {
				ByteArrayInputStream bais = new ByteArrayInputStream((byte[]) state);
                InputStream is = bais;
                if(is.read() == COMPRESSED_FLAG) {
                	is = new GZIPInputStream(is);
                }
				ois = new ObjectInputStream(is) {
					@Override
					protected Class<?> resolveClass(ObjectStreamClass desc)
					throws IOException, ClassNotFoundException {
						String name = desc.getName();
						try {
							return Class.forName(name, true,
									Thread.currentThread().getContextClassLoader());
						} catch (ClassNotFoundException cnfe) {
							Class<?> clazz = PRIMITIVE_CLASSES.get(name);
							if (clazz != null) {
								return clazz;
							} else {
								throw cnfe;
							}
						}
					}
				};
				return ois.readObject();
			} catch (Exception e) {
				throw new FacesException(e);
			} finally {
				if (ois != null) {
					try {
						ois.close();
					} catch (IOException ignored) { }
				}
			}
		} else {
			return state;
		}
	}

}

To enable it, just add the following lines in your faces config file:

<application>
    <state-manager>org.ajax4jsf.application.SearchMedicaStateManager</state-manager>
</application>

With this state manager, my web application is now using less than 1Gb of memory and is working perfectly fine! ๐Ÿ˜€

, , , , , , ,


  1. #1 by smoreau on 18 Nov 2009 - 13:30

    I created a topic on JBoss forum about this problem.
    It would be great if they add this functionality directly into the RichFaces state manager. ๐Ÿ˜‰

    Please click on the following link to check their reply: http://www.jboss.org/index.html?module=bb&op=viewtopic&t=163932

  2. #2 by smoreau on 18 Nov 2009 - 16:40

    I sent a patch for the AjaxStateManager class to JBoss and they will add my fix for the version 3.3.3 of RichFaces. ๐Ÿ™‚

    Please have a look to the Jira issue RF-8145 if you want to keep track of their progress:
    https://jira.jboss.org/jira/browse/RF-8145

  3. #3 by lucapetet on 20 May 2010 - 17:38

    Very well done. ๐Ÿ™‚

  4. #4 by Anil on 27 May 2010 - 14:27

    Thats awesome. Very helpful info.

(will not be published)