Posts Tagged memory

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):

1<context-param>
2    <param-name>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</param-name>
3    <param-value>true</param-value>
4</context-param>
5 
6<context-param>
7    <param-name>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</param-name>
8    <param-value>true</param-value>
9</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:

001package org.ajax4jsf.application;
002 
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.ObjectInputStream;
008import java.io.ObjectOutputStream;
009import java.io.ObjectStreamClass;
010import java.io.OutputStream;
011import java.util.HashMap;
012import java.util.Map;
013import java.util.zip.GZIPInputStream;
014import java.util.zip.GZIPOutputStream;
015 
016import javax.faces.FacesException;
017import javax.faces.application.StateManager;
018import javax.faces.component.UIViewRoot;
019import javax.faces.context.FacesContext;
020import javax.faces.render.ResponseStateManager;
021 
022import org.ajax4jsf.context.ContextInitParameters;
023 
024/**
025 * Overrides the Ajax state manager to enable the state compression.
026 * @author Stéphane Moreau
027 */
028public class MyAjaxStateManager extends AjaxStateManager {
029    /**
030     * Only applicable if state saving method is "server" (= default) and if
031     * <code>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</code> is
032     * <code>true</code> (= default).
033     * If <code>true</code> (default) the serialized state will be compressed before
034     * it is written to the session.
035     * If <code>false</code> the state will not be compressed.
036     */
037    private static final String COMPRESS_SERVER_STATE_PARAM = "org.apache.myfaces.COMPRESS_STATE_IN_SESSION";
038 
039    /**
040     * Default value for <code>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</code>
041     * context parameter.
042     */
043    private static final boolean DEFAULT_COMPRESS_SERVER_STATE_PARAM = true;
044 
045    private static final int UNCOMPRESSED_FLAG = 0;
046    private static final int COMPRESSED_FLAG = 1;
047 
048    private final ComponentsLoader componentLoader;
049 
050    public MyAjaxStateManager(StateManager stateManager) {
051        super(stateManager);
052        componentLoader = new ComponentsLoaderImpl();
053    }
054 
055    /**
056     * Reads the value of the <code>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</code>
057     * context parameter.
058     * @see COMPRESS_SERVER_STATE_PARAM
059     * @param context <code>FacesContext</code> for the request we are processing.
060     * @return boolean true, if the server state steam should be compressed
061     */
062    protected static boolean isCompressStateInSession(FacesContext context) {
063        String value = context.getExternalContext().getInitParameter(
064                COMPRESS_SERVER_STATE_PARAM);
065        boolean compress = DEFAULT_COMPRESS_SERVER_STATE_PARAM;
066        if (value != null) {
067            compress = Boolean.valueOf(value);
068        }
069        return compress;
070    }
071 
072    @Override
073    protected Object[] buildViewState(FacesContext context) {
074        Object[] viewStateArray = null;
075        UIViewRoot viewRoot = context.getViewRoot();
076        if (null != viewRoot && !viewRoot.isTransient()) {
077            TreeStructureNode treeStructure = (TreeStructureNode) getTreeStructureToSave(context);
078            Object state = getComponentStateToSave(context);
079            if (isSavingStateInClient(context)) {
080                viewStateArray = new Object[]{treeStructure, state};
081            } else {
082                viewStateArray = saveStateInSession(context, treeStructure,
083                        handleSaveState(context, state));
084            }
085 
086        }
087        return viewStateArray;
088    }
089 
090    @Override
091    public UIViewRoot restoreView(FacesContext context, String viewId,
092            String renderKitId) {
093        UIViewRoot viewRoot = null;
094        ResponseStateManager responseStateManager = getRenderKit(context,
095                renderKitId).getResponseStateManager();
096        TreeStructureNode treeStructure = null;
097        Object[] state = null;
098        Object[] serializedView = null;
099        if (isSavingStateInClient(context)) {
100            serializedView = (Object[]) responseStateManager.getState(context,
101                    viewId);
102 
103            if (null != serializedView) {
104                treeStructure = (TreeStructureNode) serializedView[0];
105                state = (Object[]) serializedView[1];
106            }
107        } else {
108            serializedView = restoreStateFromSession(context, viewId,
109                    renderKitId);
110 
111            if (null != serializedView) {
112                treeStructure = (TreeStructureNode) serializedView[0];
113                state = (Object[]) handleRestoreState(context, serializedView[1]);
114            }
115        }
116 
117        if (null != treeStructure) {
118            viewRoot = (UIViewRoot) treeStructure.restore(componentLoader);
119            if (null != viewRoot && null != state) {
120                viewRoot.processRestoreState(context, state[0]);
121                restoreAdditionalState(context, state[1]);
122            }
123        }
124        return viewRoot;
125 
126    }
127 
128    private static final Object handleSaveState(FacesContext context, Object state) {
129        if (ContextInitParameters.isSerializeServerState(context)) {
130            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
131            ObjectOutputStream oas = null;
132            try {
133                OutputStream os = baos;
134                if(isCompressStateInSession(context))
135                {
136                    os.write(COMPRESSED_FLAG);
137                    os = new GZIPOutputStream(os, 1024);
138                }
139                else
140                {
141                    os.write(UNCOMPRESSED_FLAG);
142                }
143 
144                oas = new ObjectOutputStream(os);
145                oas.writeObject(state);
146                oas.flush();
147            } catch (Exception e) {
148                throw new FacesException(e);
149            } finally {
150                if (oas != null) {
151                    try {
152                        oas.close();
153                    } catch (IOException ignored) { }
154                }
155            }
156            return baos.toByteArray();
157        } else {
158            return state;
159        }
160    }
161 
162    private static final Map<String,Class<?>> PRIMITIVE_CLASSES =
163        new HashMap<String,Class<?>>(9, 1.0F);
164 
165    static {
166        PRIMITIVE_CLASSES.put("boolean", boolean.class);
167        PRIMITIVE_CLASSES.put("byte", byte.class);
168        PRIMITIVE_CLASSES.put("char", char.class);
169        PRIMITIVE_CLASSES.put("short", short.class);
170        PRIMITIVE_CLASSES.put("int", int.class);
171        PRIMITIVE_CLASSES.put("long", long.class);
172        PRIMITIVE_CLASSES.put("float", float.class);
173        PRIMITIVE_CLASSES.put("double", double.class);
174        PRIMITIVE_CLASSES.put("void", void.class);
175    }
176 
177    private static final Object handleRestoreState(FacesContext context, Object state) {
178        if (ContextInitParameters.isSerializeServerState(context)) {
179            ObjectInputStream ois = null;
180            try {
181                ByteArrayInputStream bais = new ByteArrayInputStream((byte[]) state);
182                InputStream is = bais;
183                if(is.read() == COMPRESSED_FLAG) {
184                    is = new GZIPInputStream(is);
185                }
186                ois = new ObjectInputStream(is) {
187                    @Override
188                    protected Class<?> resolveClass(ObjectStreamClass desc)
189                    throws IOException, ClassNotFoundException {
190                        String name = desc.getName();
191                        try {
192                            return Class.forName(name, true,
193                                    Thread.currentThread().getContextClassLoader());
194                        } catch (ClassNotFoundException cnfe) {
195                            Class<?> clazz = PRIMITIVE_CLASSES.get(name);
196                            if (clazz != null) {
197                                return clazz;
198                            } else {
199                                throw cnfe;
200                            }
201                        }
202                    }
203                };
204                return ois.readObject();
205            } catch (Exception e) {
206                throw new FacesException(e);
207            } finally {
208                if (ois != null) {
209                    try {
210                        ois.close();
211                    } catch (IOException ignored) { }
212                }
213            }
214        } else {
215            return state;
216        }
217    }
218 
219}

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

1<application>
2    <state-manager>org.ajax4jsf.application.SearchMedicaStateManager</state-manager>
3</application>

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

, , , , , , ,

4 Comments