doortts doortts 2016-02-16
perf: Cache markdown rendering results
It is found that it is very costly job to render markdown
and sanitize it. So introduce two things.
LRU cache and compressing rendered text.

LRU cache is driven by google Guava.
Compressing text is accomplished using native java sdk.

Now, view page loading speed is about 15x faster!!
and cache size is 30% smaller.
@49e2b2ec4b8a66ed44962b6faa0cecbe5c155f17
app/utils/CacheStore.java
--- app/utils/CacheStore.java
+++ app/utils/CacheStore.java
@@ -1,10 +1,15 @@
 package utils;
 
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import models.Project;
 import models.User;
 
+import javax.annotation.Nonnull;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * CacheStore
@@ -12,4 +17,21 @@
 public class CacheStore {
     public static Map<String, User> sessionMap = new HashMap<>();
     public static Map<String, Project> projectMap = new HashMap<>();
+    public static final int MAXIMUM_CACHED_MARKDOWN_ENTRY = 10000;
+
+    /**
+     * Introduced to using LRU Cache. It depends on google Guava.
+     * <p>
+     * Size expectation: 500 char-per-item * 3 byte * 10000 rendered-entry % 70 gzipped = ~10Mb
+     */
+    public static LoadingCache<Integer, byte[]> renderedMarkdown = CacheBuilder.newBuilder()
+            .maximumSize(MAXIMUM_CACHED_MARKDOWN_ENTRY)
+            .build(
+                    new CacheLoader<Integer, byte[]>() {
+                        public Map<Integer, byte[]> renderedMarkdownMap = new ConcurrentHashMap<>();
+
+                        public byte[] load(@Nonnull Integer key) {
+                            return renderedMarkdownMap.get(key);
+                        }
+                    });
 }
app/utils/Markdown.java
--- app/utils/Markdown.java
+++ app/utils/Markdown.java
@@ -131,6 +131,9 @@
     }
 
     private static String renderWithHighlight(String source, boolean breaks) {
+        if(CacheStore.renderedMarkdown.asMap().containsKey(source.hashCode())){
+            return ZipUtil.decompress(CacheStore.renderedMarkdown.asMap().get(source.hashCode()));
+        }
         try {
             Object options = engine.eval("new Object({gfm: true, tables: true, breaks: " + breaks + ", " +
                     "pedantic: false, sanitize: false, smartLists: true," +
@@ -140,7 +143,9 @@
             String rendered = renderByMarked(source, options);
             rendered = removeJavascriptInHref(rendered);
             rendered = checkReferrer(rendered);
-            return sanitize(rendered);
+            String sanitized = sanitize(rendered);
+            CacheStore.renderedMarkdown.asMap().putIfAbsent(source.hashCode(), ZipUtil.compress(sanitized));
+            return sanitized;
         } catch (Exception ex) {
             throw new RuntimeException(ex);
         }
@@ -185,10 +190,15 @@
     }
 
     public static String render(@Nonnull String source) {
+        if(CacheStore.renderedMarkdown.asMap().containsKey(source.hashCode())){
+            return ZipUtil.decompress(CacheStore.renderedMarkdown.asMap().get(source.hashCode()));
+        }
         try {
             Object options = engine.eval("new Object({gfm: true, tables: true, breaks: true, " +
                     "pedantic: false, sanitize: false, smartLists: true});");
-            return sanitize(renderByMarked(source, options));
+            String sanitized = sanitize(renderByMarked(source, options));
+            CacheStore.renderedMarkdown.asMap().putIfAbsent(source.hashCode(), ZipUtil.compress(sanitized));
+            return sanitized;
         } catch (Exception ex) {
             throw new RuntimeException(ex);
         }
 
app/utils/ZipUtil.java (added)
+++ app/utils/ZipUtil.java
@@ -0,0 +1,38 @@
+package utils;
+
+import java.io.*;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * ZipUtil
+ *
+ * Intended to compress string contents
+ */
+public class ZipUtil {
+    public static byte[] compress(String text) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            OutputStream out = new DeflaterOutputStream(baos);
+            out.write(text.getBytes("UTF-8"));
+            out.close();
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+        return baos.toByteArray();
+    }
+
+    public static String decompress(byte[] bytes) {
+        InputStream in = new InflaterInputStream(new ByteArrayInputStream(bytes));
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[8192];
+            int len;
+            while((len = in.read(buffer))>0)
+                baos.write(buffer, 0, len);
+            return new String(baos.toByteArray(), "UTF-8");
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+    }
+}
build.sbt
--- build.sbt
+++ build.sbt
@@ -45,7 +45,7 @@
   "com.googlecode.juniversalchardet" % "juniversalchardet" % "1.0.3",
   "org.mockito" % "mockito-all" % "1.9.0" % "test",
   "com.github.zafarkhaja" % "java-semver" % "0.7.2",
-  "com.google.guava" % "guava" % "18.0",
+  "com.google.guava" % "guava" % "19.0",
   "com.googlecode.htmlcompressor" % "htmlcompressor" % "1.4",
   "org.springframework" % "spring-jdbc" % "4.1.5.RELEASE"
 )
Add a comment
List