Reduce the size (and cost) of the host page
We strip any unnecessary whitespace from the header/footer and the host page HTML, compacting it to a smaller size even before we pass it off for gzip compression. This reduces the size of final the document we have to compress and send to the browser. For anonymous users we now use a compressed anonymous copy of the host page, so we only have to dump the byte array to the network socket. This slightly improves response time for the initial page returned to the browser. For identified users we put the host page together from two different byte arrays, inserting into the middle the user's account data. This is then compressed if the browser wants gzip compression. Change-Id: I82665d9938a998ccc8b5cb2f792c8798974dcf85 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
		@@ -18,6 +18,6 @@ import com.google.gerrit.reviewdb.Account;
 | 
			
		||||
 | 
			
		||||
/** Data sent as part of the host page, to bootstrap the UI. */
 | 
			
		||||
public class HostPageData {
 | 
			
		||||
  public Account userAccount;
 | 
			
		||||
  public Account account;
 | 
			
		||||
  public GerritConfig config;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -223,8 +223,8 @@ public class Gerrit implements EntryPoint {
 | 
			
		||||
    hpd.load(new GerritCallback<HostPageData>() {
 | 
			
		||||
      public void onSuccess(final HostPageData result) {
 | 
			
		||||
        myConfig = result.config;
 | 
			
		||||
        if (result.userAccount != null) {
 | 
			
		||||
          myAccount = result.userAccount;
 | 
			
		||||
        if (result.account != null) {
 | 
			
		||||
          myAccount = result.account;
 | 
			
		||||
          applyUserPreferences();
 | 
			
		||||
        }
 | 
			
		||||
        onModuleLoad2(gStarting);
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,6 @@ import com.google.gwtjsonrpc.client.RpcImpl.Version;
 | 
			
		||||
 | 
			
		||||
@RpcImpl(version = Version.V2_0)
 | 
			
		||||
interface HostPageDataService extends RemoteJsonService {
 | 
			
		||||
  @HostPageCache(name = "gerrit_hostpagedata_obj", once = true)
 | 
			
		||||
  @HostPageCache(name = "gerrit_hostpagedata", once = true)
 | 
			
		||||
  void load(AsyncCallback<HostPageData> callback);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,10 @@ import javax.xml.transform.TransformerException;
 | 
			
		||||
import javax.xml.transform.TransformerFactory;
 | 
			
		||||
import javax.xml.transform.dom.DOMSource;
 | 
			
		||||
import javax.xml.transform.stream.StreamResult;
 | 
			
		||||
import javax.xml.xpath.XPathConstants;
 | 
			
		||||
import javax.xml.xpath.XPathExpression;
 | 
			
		||||
import javax.xml.xpath.XPathExpressionException;
 | 
			
		||||
import javax.xml.xpath.XPathFactory;
 | 
			
		||||
 | 
			
		||||
/** Utility functions to deal with HTML using W3C DOM operations. */
 | 
			
		||||
public class HtmlDomUtil {
 | 
			
		||||
@@ -146,7 +150,9 @@ public class HtmlDomUtil {
 | 
			
		||||
    try {
 | 
			
		||||
      try {
 | 
			
		||||
        try {
 | 
			
		||||
          return newBuilder().parse(in);
 | 
			
		||||
          final Document doc = newBuilder().parse(in);
 | 
			
		||||
          compact(doc);
 | 
			
		||||
          return doc;
 | 
			
		||||
        } catch (SAXException e) {
 | 
			
		||||
          throw new IOException("Error reading " + name, e);
 | 
			
		||||
        } catch (ParserConfigurationException e) {
 | 
			
		||||
@@ -160,6 +166,21 @@ public class HtmlDomUtil {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static void compact(final Document doc) {
 | 
			
		||||
    try {
 | 
			
		||||
      final String expr = "//text()[normalize-space(.) = '']";
 | 
			
		||||
      final XPathFactory xp = XPathFactory.newInstance();
 | 
			
		||||
      final XPathExpression e = xp.newXPath().compile(expr);
 | 
			
		||||
      NodeList empty = (NodeList) e.evaluate(doc, XPathConstants.NODESET);
 | 
			
		||||
      for (int i = 0; i < empty.getLength(); i++) {
 | 
			
		||||
        Node node = empty.item(i);
 | 
			
		||||
        node.getParentNode().removeChild(node);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (XPathExpressionException e) {
 | 
			
		||||
      // Don't do the whitespace removal.
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Read a Read a UTF-8 text file from our CLASSPATH and return it. */
 | 
			
		||||
  public static String readFile(final Class<?> context, final String name)
 | 
			
		||||
      throws IOException {
 | 
			
		||||
@@ -175,17 +196,14 @@ public class HtmlDomUtil {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Parse an XHTML file from the local drive and return the instance. */
 | 
			
		||||
  public static Document parseFile(final File parentDir, final String name)
 | 
			
		||||
      throws IOException {
 | 
			
		||||
    if (parentDir == null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    final File path = new File(parentDir, name);
 | 
			
		||||
  public static Document parseFile(final File path) throws IOException {
 | 
			
		||||
    try {
 | 
			
		||||
      final InputStream in = new FileInputStream(path);
 | 
			
		||||
      try {
 | 
			
		||||
        try {
 | 
			
		||||
          return newBuilder().parse(in);
 | 
			
		||||
          final Document doc = newBuilder().parse(in);
 | 
			
		||||
          compact(doc);
 | 
			
		||||
          return doc;
 | 
			
		||||
        } catch (SAXException e) {
 | 
			
		||||
          throw new IOException("Error reading " + path, e);
 | 
			
		||||
        } catch (ParserConfigurationException e) {
 | 
			
		||||
@@ -239,6 +257,7 @@ public class HtmlDomUtil {
 | 
			
		||||
    factory.setValidating(false);
 | 
			
		||||
    factory.setExpandEntityReferences(false);
 | 
			
		||||
    factory.setIgnoringComments(true);
 | 
			
		||||
    factory.setCoalescing(true);
 | 
			
		||||
    final DocumentBuilder parser = factory.newDocumentBuilder();
 | 
			
		||||
    return parser;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
  private static final Logger log =
 | 
			
		||||
      LoggerFactory.getLogger(HostPageServlet.class);
 | 
			
		||||
  private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
 | 
			
		||||
  private static final String HPD_ID = "gerrit_hostpagedata";
 | 
			
		||||
 | 
			
		||||
  private final Provider<CurrentUser> currentUser;
 | 
			
		||||
  private final GerritConfig config;
 | 
			
		||||
@@ -120,31 +121,8 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
    asScript(scriptNode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void injectJson(final Document hostDoc, final String id,
 | 
			
		||||
      final Object obj) {
 | 
			
		||||
    final Element scriptNode = HtmlDomUtil.find(hostDoc, id);
 | 
			
		||||
    if (scriptNode == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (scriptNode.getFirstChild() != null) {
 | 
			
		||||
      scriptNode.removeChild(scriptNode.getFirstChild());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (obj == null) {
 | 
			
		||||
      scriptNode.getParentNode().removeChild(scriptNode);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final StringWriter w = new StringWriter();
 | 
			
		||||
    w.write("<!--\n");
 | 
			
		||||
    w.write("var ");
 | 
			
		||||
    w.write(id);
 | 
			
		||||
    w.write("_obj=");
 | 
			
		||||
    JsonServlet.defaultGsonBuilder().create().toJson(obj, w);
 | 
			
		||||
    w.write(";\n// -->\n");
 | 
			
		||||
    asScript(scriptNode);
 | 
			
		||||
    scriptNode.appendChild(hostDoc.createCDATASection(w.toString()));
 | 
			
		||||
  private void json(final Object data, final StringWriter w) {
 | 
			
		||||
    JsonServlet.defaultGsonBuilder().create().toJson(data, w);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static void asScript(final Element scriptNode) {
 | 
			
		||||
@@ -172,22 +150,26 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
  @Override
 | 
			
		||||
  protected void doGet(final HttpServletRequest req,
 | 
			
		||||
      final HttpServletResponse rsp) throws IOException {
 | 
			
		||||
    final HostPageData pageData = new HostPageData();
 | 
			
		||||
    pageData.config = config;
 | 
			
		||||
    final Page page = get();
 | 
			
		||||
    final byte[] raw;
 | 
			
		||||
 | 
			
		||||
    final CurrentUser user = currentUser.get();
 | 
			
		||||
    if (user instanceof IdentifiedUser) {
 | 
			
		||||
      pageData.userAccount = ((IdentifiedUser) user).getAccount();
 | 
			
		||||
      final StringWriter w = new StringWriter();
 | 
			
		||||
      w.write(HPD_ID + ".account=");
 | 
			
		||||
      json(((IdentifiedUser) user).getAccount(), w);
 | 
			
		||||
      w.write(";");
 | 
			
		||||
      final byte[] userData = w.toString().getBytes("UTF-8");
 | 
			
		||||
 | 
			
		||||
      raw = concat(page.part1, userData, page.part2);
 | 
			
		||||
    } else {
 | 
			
		||||
      raw = page.full;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final Document peruser = HtmlDomUtil.clone(get().hostDoc);
 | 
			
		||||
    injectJson(peruser, "gerrit_hostpagedata", pageData);
 | 
			
		||||
 | 
			
		||||
    final byte[] raw = HtmlDomUtil.toUTF8(peruser);
 | 
			
		||||
    final byte[] tosend;
 | 
			
		||||
    if (RPCServletUtils.acceptsGzipEncoding(req)) {
 | 
			
		||||
      rsp.setHeader("Content-Encoding", "gzip");
 | 
			
		||||
      tosend = HtmlDomUtil.compress(raw);
 | 
			
		||||
      tosend = raw == page.full ? page.full_gz : HtmlDomUtil.compress(raw);
 | 
			
		||||
    } else {
 | 
			
		||||
      tosend = raw;
 | 
			
		||||
    }
 | 
			
		||||
@@ -206,6 +188,20 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static byte[] concat(byte[] p1, byte[] p2, byte[] p3) {
 | 
			
		||||
    final byte[] r = new byte[p1.length + p2.length + p3.length];
 | 
			
		||||
    int p = 0;
 | 
			
		||||
    p = append(p1, r, p);
 | 
			
		||||
    p = append(p2, r, p);
 | 
			
		||||
    p = append(p3, r, p);
 | 
			
		||||
    return r;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static int append(byte[] src, final byte[] dst, int p) {
 | 
			
		||||
    System.arraycopy(src, 0, dst, p, src.length);
 | 
			
		||||
    return p + src.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static class FileInfo {
 | 
			
		||||
    private final File path;
 | 
			
		||||
    private final long time;
 | 
			
		||||
@@ -221,17 +217,47 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private class Page {
 | 
			
		||||
    final Document hostDoc;
 | 
			
		||||
 | 
			
		||||
    private final FileInfo css;
 | 
			
		||||
    private final FileInfo header;
 | 
			
		||||
    private final FileInfo footer;
 | 
			
		||||
 | 
			
		||||
    final byte[] part1;
 | 
			
		||||
    final byte[] part2;
 | 
			
		||||
    final byte[] full;
 | 
			
		||||
    final byte[] full_gz;
 | 
			
		||||
 | 
			
		||||
    Page() throws IOException {
 | 
			
		||||
      hostDoc = HtmlDomUtil.clone(template);
 | 
			
		||||
      Document hostDoc = HtmlDomUtil.clone(template);
 | 
			
		||||
 | 
			
		||||
      css = injectCssFile(hostDoc, "gerrit_sitecss", site.site_css);
 | 
			
		||||
      header = injectXmlFile(hostDoc, "gerrit_header", site.site_header);
 | 
			
		||||
      footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
 | 
			
		||||
 | 
			
		||||
      final HostPageData pageData = new HostPageData();
 | 
			
		||||
      pageData.config = config;
 | 
			
		||||
 | 
			
		||||
      final StringWriter w = new StringWriter();
 | 
			
		||||
      w.write("var " + HPD_ID + "=");
 | 
			
		||||
      json(pageData, w);
 | 
			
		||||
      w.write(";");
 | 
			
		||||
 | 
			
		||||
      final Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
 | 
			
		||||
      if (data == null) {
 | 
			
		||||
        throw new IOException("No " + HPD_ID + " in host page HTML");
 | 
			
		||||
      }
 | 
			
		||||
      asScript(data);
 | 
			
		||||
      data.appendChild(hostDoc.createTextNode(w.toString()));
 | 
			
		||||
      data.appendChild(hostDoc.createComment(HPD_ID));
 | 
			
		||||
 | 
			
		||||
      final String raw = HtmlDomUtil.toString(hostDoc);
 | 
			
		||||
      final int p = raw.indexOf("<!--" + HPD_ID);
 | 
			
		||||
      if (p < 0) {
 | 
			
		||||
        throw new IOException("No tag in transformed host page HTML");
 | 
			
		||||
      }
 | 
			
		||||
      part1 = raw.substring(0, p).getBytes("UTF-8");
 | 
			
		||||
      part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
 | 
			
		||||
      full = concat(part1, part2, new byte[0]);
 | 
			
		||||
      full_gz = HtmlDomUtil.compress(full);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isStale() {
 | 
			
		||||
@@ -273,7 +299,7 @@ public class HostPageServlet extends HttpServlet {
 | 
			
		||||
        banner.removeChild(banner.getFirstChild());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      Document html = HtmlDomUtil.parseFile(src.getParentFile(), src.getName());
 | 
			
		||||
      Document html = HtmlDomUtil.parseFile(src);
 | 
			
		||||
      if (html == null) {
 | 
			
		||||
        banner.getParentNode().removeChild(banner);
 | 
			
		||||
        return info;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user