We had an issue in production. We are producing reports using JasperReports. Some reports were not being generated under load. The application has a REST interface to produce a report. We had a specific 75 page documents which contains a two SVG logos on each page and 47 photos throughout the report.
We quickly started thinking that the amount of heap space used to generate the report was probably causing problems. So we tried some measurements. When simply starting the application and then generating the report, the application needed a heap of 460MB to succeed. Time to investigate and look for optimizations.
We improved the following:
- Upgraded JasperReports to the latest version.
- The images which needed to be included in the report were being sent as part of the request using base 64 encoding for the image. This request was logged. We improved this by
- Removing the logging of the base 64 data.
- Next we changed this by letting the application load the images as late as possible by including download details instead of base64 data. The image is now lazy-loaded to allow the image data to be garbage collected quickly.
- The (svg) images which are displayed in the header and footer of each page now use the “isCached” attribute to allow JasperReports to cache the images instead of re-rendering for each page.
- Our reports use styled text for all fields. However most fields do not contain any styling. To avoid excessive parsing of styled text (full XML parsing) we patched the JRStyledTextParser class JasperReports to only parse the text when it contains at least both a < and a > See the excerpt below. This is one change we measured separately and it saves us 10MB of heap space.
- We enabled page virtualization. This reduces the number of pages which are kept in memory while rendering.
With these changes we were able to improve the situation substantially. It is now possible to start the application and generate a report with a heap of only 120MB.
The following patch was applied to JRStyledTextParser (adding one “if” to getStyledText).
public JRStyledText getStyledText(Map<Attribute,Object> parentAttributes, String text, boolean isStyledText, Locale locale) { JRStyledText styledText = null; if (isStyledText && text.contains("<") && text.contains(">")) // changed JVDA to prevent excessive parsing { try { styledText = parse(parentAttributes, text, locale); } catch (SAXException e) { //ignore if invalid styled text and treat like normal text } } if (styledText == null) { // using the original String object instead without creating a buffer and a String copy styledText = new JRStyledText(locale, text, parentAttributes); } return styledText; } |
The lazy loading of the images is handles by wrapping the parameters which are passed to JasperReports. This basically delegates to the underlying map
/** * Special map for inside rowMaps which loads external images lazily. */ public class LazyLoadExternalImagesMap implements Map<String, Object> { private static final String EXTERNAL_IMAGE = "externalImage"; private static final String EXTERNAL_IMAGE_REF = "externalImageRef"; private static final String EXTERNAL_IMAGE_MIME = "mimeType"; private static final String EXTERNAL_IMAGE_URL = "url"; private static final String EXTERNAL_IMAGE_COOKIE = "cookie"; private Map<String, Object> delegate; private ExternalDataHelper externalDataHelper; /** * Constructor. * * @param delegate delegate * @param externalDataHelper helper to load the external image */ public LazyLoadExternalImagesMap(Map<String, Object> delegate, ExternalDataHelper externalDataHelper) { this.delegate = delegate; this.externalDataHelper = externalDataHelper; } @Override public Object get(Object key) { if (EXTERNAL_IMAGE.equals(key)) { ExternalImageTo eb = getExternalImageRef(); if (null != eb) { InputStream is = externalDataHelper.get(eb); try { return ImageIO.read(is); } catch (IOException ioe) { throw new ServiceException("Cannot load external image " + eb + ".", ioe); } finally { StreamUtil.close(is); } } } return delegate.get(key); } private ExternalImageTo getExternalImageRef() { Object ebRef = delegate.get(EXTERNAL_IMAGE_REF); if (ebRef instanceof Map) { Map map = (Map) ebRef; if (map.containsKey(EXTERNAL_IMAGE_MIME) && map.containsKey(EXTERNAL_IMAGE_URL)) { ExternalImageTo eb = new ExternalImageTo(); eb.setMimeType(string(map.get(EXTERNAL_IMAGE_MIME))); eb.setUrl(string(map.get(EXTERNAL_IMAGE_URL))); eb.setCookie(string(map.get(EXTERNAL_IMAGE_COOKIE))); return eb; } } return null; } private String string(Object s) { if (null == s) { return null; } return s.toString(); } // ----- delegated methods below @Override public int size() { return delegate.size(); } @Override public boolean isEmpty() { return delegate.isEmpty(); } @Override public boolean containsKey(Object key) { return delegate.containsKey(key); } @Override public boolean containsValue(Object value) { return delegate.containsValue(value); } @Override public Object put(String key, Object value) { return delegate.put(key, value); } @Override public Object remove(Object key) { return delegate.remove(key); } @Override public void putAll(Map<? extends String, ?> m) { delegate.putAll(m); } @Override public void clear() { delegate.clear(); } @Override public Set<String> keySet() { return delegate.keySet(); } @Override public Collection<Object> values() { return delegate.values(); } @Override public Set<Entry<String, Object>> entrySet() { return delegate.entrySet(); } @Override public boolean equals(Object o) { return delegate.equals(o); } @Override public int hashCode() { return delegate.hashCode(); } @Override public Object getOrDefault(Object key, Object defaultValue) { return delegate.getOrDefault(key, defaultValue); } @Override public void forEach(BiConsumer<? super String, ? super Object> action) { delegate.forEach(action); } @Override public void replaceAll(BiFunction<? super String, ? super Object, ?> function) { delegate.replaceAll(function); } @Override public Object putIfAbsent(String key, Object value) { return delegate.putIfAbsent(key, value); } @Override public boolean remove(Object key, Object value) { return delegate.remove(key, value); } @Override public boolean replace(String key, Object oldValue, Object newValue) { return delegate.replace(key, oldValue, newValue); } @Override public Object replace(String key, Object value) { return delegate.replace(key, value); } @Override public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) { return delegate.computeIfAbsent(key, mappingFunction); } @Override public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) { return delegate.computeIfPresent(key, remappingFunction); } @Override public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) { return delegate.compute(key, remappingFunction); } @Override public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) { return delegate.merge(key, value, remappingFunction); } } |
Leave a Reply