In a high concurrency load test with Apache MyFaces + RichFaces component library we found that under peak load majority of our web container threads were stuck in this call stack
at java/util/zip/Inflater.inflateBytes(Native Method) at java/util/zip/Inflater.inflate(Inflater.java:249(Compiled Code)) at java/util/zip/InflaterInputStream.read(InflaterInputStream.java:146(Compiled Code)) at java/util/zip/InflaterInputStream.read(InflaterInputStream.java:116(Compiled Code)) at java/io/FilterInputStream.read(FilterInputStream.java:77(Compiled Code)) at java/io/FilterInputStream.read(FilterInputStream.java:77(Compiled Code)) at java/io/PushbackInputStream.read(PushbackInputStream.java:133(Compiled Code)) at org/apache/myfaces/shared_impl/resource/ResourceImpl$ValueExpressionFilterInputStream.read(ResourceImpl.java:117(Compiled Code)) at java/io/InputStream.read(InputStream.java:175(Compiled Code)) at java/io/InputStream.read(InputStream.java:97(Compiled Code)) at org/apache/myfaces/application/ResourceHandlerImpl.pipeBytes(ResourceHandlerImpl.java:402(Compiled Code)) at org/apache/myfaces/application/ResourceHandlerImpl.handleResourceRequest(ResourceHandlerImpl.java:357(Compiled Code)) at org/richfaces/resource/ResourceHandlerImpl.handleResourceRequest(ResourceHandlerImpl.java:257(Compiled Code)) at javax/faces/webapp/FacesServlet.service(FacesServlet.java:183(Compiled Code)) at org/richfaces/webapp/ResourceServlet.httpService(ResourceServlet.java:110(Compiled Code)) at org/richfaces/webapp/ResourceServlet.service(ResourceServlet.java:105(Compiled Code)) After looking at the src code of org.apache.myfaces.application.ResourceHandlerImpl.handleResourceRequest(FacesContext) I can see that the ResourceHandlerCache caches the Resource metadata ; however the actual output of the resource which is written to the outputstream in ResourceHandlerImpl.pipeBytes is NEVER cached. This causes a scalability problem in our case because each access to the resource involves cracking open a jar, inflating the bytes and parsing a stream of bytes. This is done multiple times for the same resource(say a css file) inside the richfaces jar inspite of the resource not changing. I propose a much needed performance optimization wherein we cache the output of the Resource handled and stash the output byte[] it as softReference in the ResourceHandlerCache.ResourceValue. I have attached a patch that does the same and would like your feedback on my proposal. These patches are from src taken from Apache MyFaces 2.0.5 -cheers, Rohit Kelapure, Apache Open WebBeans committer
*** C:\temp\old\ResourceHandlerCache.java 2011-03-08 08:40:12.000000000 -0400 --- C:\temp\new\ResourceHandlerCache.java 2012-07-17 20:52:23.000000000 -0400 *************** *** 15,26 **** --- 15,27 ---- * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.myfaces.shared_impl.resource; + import java.lang.ref.SoftReference; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; *************** *** 36,46 **** --- 37,51 ---- private static final Logger log = Logger .getLogger(ResourceHandlerCache.class.getName()); private Boolean _resourceCacheEnabled = null; private Map<ResourceKey, ResourceValue> _resourceCacheMap = null; + public Map<ResourceKey, ResourceValue> getResourceCacheMap() { + return _resourceCacheMap; + } @JSFWebConfigParam(defaultValue = "500", since = "2.0.2") private static final String RESOURCE_HANDLER_CACHE_SIZE_ATTRIBUTE = "org.apache.myfaces.RESOURCE_HANDLER_CACHE_SIZE"; private static final int RESOURCE_HANDLER_CACHE_DEFAULT_SIZE = 500; @JSFWebConfigParam(defaultValue = "true", since = "2.0.2") *************** *** 134,144 **** --- 139,155 ---- return WebConfigParamUtils.getIntegerInitParameter(externalContext, RESOURCE_HANDLER_CACHE_SIZE_ATTRIBUTE, RESOURCE_HANDLER_CACHE_DEFAULT_SIZE); } public static class ResourceKey { + @Override + public String toString() { + return "ResourceKey [contentType=" + contentType + ", libraryName=" + + libraryName + ", resourceName=" + resourceName + "]"; + } private String resourceName; private String libraryName; private String contentType; public ResourceKey(String resourceName, String libraryName, *************** *** 201,211 **** --- 212,234 ---- public static class ResourceValue { private ResourceMeta resourceMeta; private ResourceLoader resourceLoader; + private SoftReference<byte[]> outputReference; + public byte[] getOutput() + { + return outputReference != null ? outputReference.get() : null; + } + public void setOutput(byte[] o) + { + this.outputReference = new SoftReference<byte[]>(o); + } public ResourceValue(ResourceMeta resourceMeta, ResourceLoader resourceLoader) { super(); this.resourceMeta = resourceMeta;
*** C:\temp\old\ResourceHandlerImpl.java 2012-01-25 19:03:58.000000000 -0400 --- C:\temp\new\ResourceHandlerImpl.java 2012-07-17 23:53:38.000000000 -0400 *************** *** 15,53 **** * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.myfaces.application; ! import org.apache.myfaces.renderkit.ErrorPageWriter; ! import org.apache.myfaces.shared_impl.resource.ResourceHandlerCache; ! import org.apache.myfaces.shared_impl.resource.ResourceHandlerCache.ResourceValue; ! import org.apache.myfaces.shared_impl.resource.ResourceHandlerSupport; ! import org.apache.myfaces.shared_impl.resource.ResourceImpl; ! import org.apache.myfaces.shared_impl.resource.ResourceLoader; ! import org.apache.myfaces.shared_impl.resource.ResourceMeta; ! import org.apache.myfaces.shared_impl.util.ClassUtils; ! import org.apache.myfaces.shared_impl.util.ExternalContextUtils; ! import org.apache.myfaces.shared_impl.util.StringUtils; - import javax.faces.application.Resource; - import javax.faces.application.ResourceHandler; - import javax.faces.application.ResourceWrapper; - import javax.faces.context.ExternalContext; - import javax.faces.context.FacesContext; - import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; /** * DOCUMENT ME! * * @author Simon Lessard (latest modification by $Author: slessard $) * --- 15,55 ---- * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.myfaces.application; ! import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; + import javax.faces.application.Resource; + import javax.faces.application.ResourceHandler; + import javax.faces.application.ResourceWrapper; + import javax.faces.context.ExternalContext; + import javax.faces.context.FacesContext; + import javax.servlet.http.HttpServletResponse; + import org.apache.myfaces.renderkit.ErrorPageWriter; + import org.apache.myfaces.shared_impl.resource.ResourceHandlerCache; + import org.apache.myfaces.shared_impl.resource.ResourceHandlerSupport; + import org.apache.myfaces.shared_impl.resource.ResourceImpl; + import org.apache.myfaces.shared_impl.resource.ResourceLoader; + import org.apache.myfaces.shared_impl.resource.ResourceMeta; + import org.apache.myfaces.shared_impl.resource.ResourceHandlerCache.ResourceValue; + import org.apache.myfaces.shared_impl.util.ClassUtils; + import org.apache.myfaces.shared_impl.util.ExternalContextUtils; + import org.apache.myfaces.shared_impl.util.StringUtils; /** * DOCUMENT ME! * * @author Simon Lessard (latest modification by $Author: slessard $) * *************** *** 262,272 **** --- 264,276 ---- * This method implements an algorithm semantically identical to * the one described on the javadoc of ResourceHandler.handleResourceRequest */ @Override public void handleResourceRequest(FacesContext facesContext) throws IOException { + log.entering(this.getClass().getName(), "handleResourceRequest"); try { String resourceBasePath = getResourceHandlerSupport() .calculateResourceBasePath(facesContext); *************** *** 327,341 **** --- 332,353 ---- if (resource == null) { httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } + log.finer("\t NAME "+ resource.getResourceName()); + log.finer("\t ContentType "+ resource.getContentType()); + log.finer("\t Library "+ resource.getLibraryName()); + log.finer("\t Request Path "+ resource.getRequestPath()); + log.finer("\t URL "+ resource.getURL()); if (!resource.userAgentNeedsUpdate(facesContext)) { httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + log.exiting(this.getClass().getName(), "handleResourceRequest", "SC_NOT_MODIFIED"); return; } httpServletResponse.setContentType(_getContentType(resource, facesContext.getExternalContext())); Map<String, String> headers = resource.getResponseHeaders(); *************** *** 347,363 **** //serve up the bytes (taken from trinidad ResourceServlet) try { InputStream in = resource.getInputStream(); OutputStream out = httpServletResponse.getOutputStream(); byte[] buffer = new byte[_BUFFER_SIZE]; ! try { ! int count = pipeBytes(in, out, buffer); ! //set the content lenght httpServletResponse.setContentLength(count); } finally { try --- 359,396 ---- //serve up the bytes (taken from trinidad ResourceServlet) try { InputStream in = resource.getInputStream(); OutputStream out = httpServletResponse.getOutputStream(); + ResourceValue resourceValue = getResourceLoaderCache().getResource(resource.getResourceName(), resource.getLibraryName(), resource.getContentType()); + log.finer("ResourceValue "+ resourceValue); byte[] buffer = new byte[_BUFFER_SIZE]; ! int count = -1; ! log.finer(getResourceLoaderCache().getResourceCacheMap().toString()); try { + if (null != resourceValue && + resourceValue.getResourceMeta().couldResourceContainValueExpressions()) + { + byte[] b = resourceValue.getOutput(); + if (null != b) + { + out.write(b, 0, b.length); + count = b.length; + log.finer("CACHE HIT- SERVING UP BYTES "+ count); ! } else { ! count = pipeAndCacheBytes(in, out, buffer); ! resourceValue.setOutput(buffer); ! log.finer("CACHE MISS - SERVING UP BYTES "+ count); ! } ! } else { ! count = pipeBytes(in, out, buffer); ! log.finer("NONE SERVING UP BYTES "+ count); ! } httpServletResponse.setContentLength(count); } finally { try *************** *** 386,403 **** // FIXME we are creating a html error page for a non html request here // shouln't we do something better? -=Jakob Korherr=- ErrorPageWriter.handleThrowable(facesContext, ex); } } /** * Reads the specified input stream into the provided byte array storage and * writes it to the output stream. */ ! private static int pipeBytes(InputStream in, OutputStream out, byte[] buffer) ! throws IOException { int count = 0; int length; while ((length = (in.read(buffer))) >= 0) { --- 419,459 ---- // FIXME we are creating a html error page for a non html request here // shouln't we do something better? -=Jakob Korherr=- ErrorPageWriter.handleThrowable(facesContext, ex); } + log.exiting(this.getClass().getName(), "handleResourceRequest"); } /** * Reads the specified input stream into the provided byte array storage and * writes it to the output stream. */ ! private static int pipeAndCacheBytes(InputStream is, OutputStream out, byte[] data) throws IOException { ! ! ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + data = buffer.toByteArray(); + out.write(data, 0, data.length); + return data.length; + } /** * Reads the specified input stream into the provided byte array storage and * writes it to the output stream. */ + private static int pipeBytes(InputStream in, OutputStream out, byte[] buffer) throws IOException { int count = 0; int length; while ((length = (in.read(buffer))) >= 0) {